# meridian-euid (Python)

[![Python CI](https://github.com/lsmc-bio/meridian-euid/actions/workflows/python-ci.yml/badge.svg)](https://github.com/lsmc-bio/meridian-euid/actions/workflows/python-ci.yml)
[![GitHub Release](https://img.shields.io/github/v/release/lsmc-bio/meridian-euid?style=flat-square&label=release)](https://github.com/lsmc-bio/meridian-euid/releases/latest)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)

Non-normative Python reference implementation of the [Meridian EUID specification](../../SPEC.md).

## Requirements

- Python 3.10+

## Installation

### From PyPI (when published)

```bash
pip install meridian-euid
```

### From GitHub

```bash
pip install git+https://github.com/lsmc-bio/meridian-euid.git#subdirectory=reference-implementations/python
```

### From source (development)

```bash
git clone https://github.com/lsmc-bio/meridian-euid.git
cd meridian-euid/reference-implementations/python

# Install package
pip install -e .

# With dev dependencies (pytest, ruff, mypy, etc.)
pip install -e ".[dev]"
```

## CLI Usage

The `meridian-euid` command provides four subcommands:

### validate

Validate an EUID against production or sandbox rules.

```bash
# Production EUID
meridian-euid validate TX-1C --env production
# Output: VALID: TX-1C

# Sandbox EUID with allowed prefixes
meridian-euid validate X:TAP-3VX --env sandbox --allowed-prefixes X
# Output: VALID: X:TAP-3VX

# Multiple allowed prefixes
meridian-euid validate H:DNA-E8E --env sandbox --allowed-prefixes H,Q

# Invalid EUID (exits with code 1)
meridian-euid validate TX-1D --env production
# Output: INVALID: checksum mismatch
```

| Option | Required | Description |
|--------|----------|-------------|
| `--env` | Yes | `production` or `sandbox` |
| `--allowed-prefixes` | No | Comma-separated sandbox prefixes (sandbox only) |

**Exit codes:** `0` = valid, `1` = invalid

### encode

Encode a positive integer as a Meridian EUID.

```bash
# Production EUID
meridian-euid encode 12345 DNA
# Output: DNA-C1SJ

# Sandbox EUID
meridian-euid encode 123 TAP --sandbox X
# Output: X:TAP-3VX
```

| Argument/Option | Required | Description |
|-----------------|----------|-------------|
| `integer` | Yes | Positive integer to encode |
| `category` | Yes | Category prefix (2–3 uppercase Crockford letters) |
| `--sandbox` | No | Sandbox prefix (H–Z, excluding I/L/O/U; A–G reserved) |

### parse

Parse a validated EUID into its components (outputs JSON).

```bash
meridian-euid parse DNA-C1SJ
# Output: {"sandbox": null, "category": "DNA", "body": "C1S", "checksum": "J", "integer": 12345}

meridian-euid parse X:TAP-3VX
# Output: {"sandbox": "X", "category": "TAP", "body": "3V", "checksum": "X", "integer": 123}
```

### compute-check

Compute the check character for a payload.

```bash
# Production payload (CATEGORY + BODY)
meridian-euid compute-check TX1
# Output: C

# Sandbox payload (SANDBOX + CATEGORY + BODY; no ':' or '-')
meridian-euid compute-check XTAP3V
# Output: X
```

## Programmatic API

### Functions

```python
from meridian_euid import validate, is_valid, compute_check_character, encode, parse

# validate() - returns input on success, raises on failure
euid = validate("TX-1C", environment="production")

# is_valid() - returns True/False
if is_valid("TX-1C", environment="production"):
    print("Valid!")

# encode() - encode integer as EUID
euid = encode(12345, "DNA")                     # "DNA-C1SJ"
euid = encode(123, "TAP", sandbox="X")          # "X:TAP-3VX"

# parse() - parse EUID into components
result = parse("DNA-C1SJ")
# {"sandbox": None, "category": "DNA", "body": "C1S", "checksum": "J", "integer": 12345}

# compute_check_character() - compute check character for payload
check = compute_check_character("TX1")  # Returns "C"

# Sandbox validation with prefix restriction
validate("X:TAP-3VX", environment="sandbox", allowed_sandbox_prefixes={"X"})
```

### Exceptions

```python
from meridian_euid import EUIDError, EUIDFormatError, EUIDChecksumError, EUIDSandboxPrefixError
```

| Exception | Raised when |
|-----------|-------------|
| `EUIDFormatError` | Regex or canonical form failure |
| `EUIDChecksumError` | Checksum mismatch or forbidden `*` |
| `EUIDSandboxPrefixError` | Sandbox prefix not in allowed set |

All exceptions inherit from `EUIDError`.

## Running Tests

```bash
# Run tests with coverage (requires dev dependencies)
pytest --cov --cov-report=term-missing

# Run linting and type checks
ruff check meridian_euid tests
ruff format --check meridian_euid tests
mypy meridian_euid --ignore-missing-imports
```

Tests use shared test vectors from [`test-vectors/v2.json`](../../test-vectors/v2.json).

Coverage threshold: **75%** (configured in `pyproject.toml`)

## Related Documentation

- [SPEC.md](../../SPEC.md) — Normative specification
- [Test Vectors](../../test-vectors/README.md) — Shared test cases
- [TypeScript Implementation](../typescript/README.md) — TypeScript/Node.js version
