Metadata-Version: 2.4
Name: lr-serial
Version: 0.6.0
Summary: CLI client for interacting with LumenRadio serial generation backend service.
Author-email: Jonas Estberger <jonas.estberger@lumenradio.com>
License: MIT License
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.32
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13.7
Requires-Dist: pydantic>=2.8
Provides-Extra: dev
Requires-Dist: build>=1.2.1; extra == "dev"
Requires-Dist: twine>=5.1.1; extra == "dev"
Requires-Dist: wheel; extra == "dev"
Requires-Dist: pytest>=8.4.2; extra == "dev"
Requires-Dist: black>=25.9.0; extra == "dev"
Requires-Dist: pytest-html; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Dynamic: license-file


# lr-serial

CLI + Python client for interacting with the LumenRadio Serial Number Generation Service.

## Install

```bash
pip install lr-serial
```

## AI assistant integration

`lr-serial` ships a bundled skill/instruction file that teaches AI coding assistants
about the CLI commands, Python API, data models, and common workflows.

Run this once from the root of your project:

```bash
serial --install-skill
```

This does three things:

- **Claude Code** — copies the skill to `~/.claude/skills/lr-serial/` (global, available in all projects)
- **GitHub Copilot** — writes `.github/instructions/lr-serial.instructions.md` in the current directory
- **VSCode** — sets `github.copilot.chat.codeGeneration.useInstructionFiles: true` in `.vscode/settings.json` (creates the file if it doesn't exist; merges if it does)

To inspect the skill content without installing:

```bash
serial --show-skill
```

---

## Python API

`lr-serial` exposes a `SerialClient` class for programmatic use — no CLI parsing, no `sys.exit()`.
All errors are raised as typed exceptions from `serial.exceptions`.

### Quick start

```python
from serial import SerialClient

# From stored credentials (after `serial login`)
client = SerialClient.from_config()

# Or from M2M credentials directly
client = SerialClient.from_m2m(
    client_id="<CLIENT_ID>",
    client_secret="<CLIENT_SECRET>",
    token_url="https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token",
)

# List articles
articles = client.list_articles()   # -> list[ArticleListItem]

# Generate serial numbers
serials = client.generate("PROD-01", quantity=5, fields={"site": "Stockholm"})  # -> list[str]

# Look up a serial
info = client.lookup("PROD-STH-000001")  # -> SerialInfo

# Full article details
article = client.article_get("PROD-01")  # -> Article
```

### Exception handling

```python
from serial.exceptions import AuthenticationError, NotFoundError, SerialError

try:
    info = client.lookup("UNKNOWN-000")
except NotFoundError:
    print("Serial not found")
except AuthenticationError:
    print("Token expired — run `serial login` again")
except SerialError as exc:
    print(f"API error: {exc}")
```

### Reference

| Method | Returns | Description |
|---|---|---|
| `SerialClient.from_config(path?)` | `SerialClient` | Load from `~/.config/serial/login.json` |
| `SerialClient.from_m2m(client_id, client_secret, token_url, scope?)` | `SerialClient` | OAuth2 client credentials |
| `client.list_articles()` | `list[ArticleListItem]` | All articles |
| `client.article_get(article_no)` | `Article` | Full article details |
| `client.article_create(article_no, description, prefix, schema, disabled?)` | `dict` | Create article |
| `client.article_update(article_no, *, description?, prefix?, disabled?, schema?)` | `dict` | Partial update |
| `client.article_delete(article_no)` | `None` | Delete article |
| `client.article_set_sequence(article_no, sequence_no)` | `dict` | Set serial counter |
| `client.generate(article_no, quantity?, fields?)` | `list[str]` | Generate serials |
| `client.lookup(serial_no)` | `SerialInfo` | Serial metadata |
| `client.site_list()` | `list[Site]` | All sites |
| `client.site_create(site_no, name)` | `dict` | Create site |
| `client.site_update(site_no, name, *, client_id?, contact_email?, disabled?)` | `dict` | Update site |
| `client.site_delete(site_no)` | `None` | Delete site |
| `client.audit_list(limit?)` | `list[AuditEntry]` | Audit log (admin scope) |
| `client.audit_restore(audit_id)` | `dict` | Undo audited change (admin scope) |

### Models

| Model | Key fields |
|---|---|
| `ArticleListItem` | `article`, `description` |
| `Article` | `article`, `description`, `prefix`, `disabled`, `schema_` |
| `ArticleSchema` | `version`, `format`, `regexp`, `fields` |
| `SerialInfo` | `serial_no`, `article`, `created_by`, `created_at` |
| `Site` | `site_no`, `name`, `client_id`, `contact_email`, `disabled` |
| `AuditEntry` | `id`, `touched_table`, `touched_key`, `change_type`, `changed_by`, `changed_at` |

---

## CLI Usage

### Login Command

The `login` command supports two authentication modes:

#### User login (default)

Opens the system browser for interactive sign-in via OAuth 2.0 Authorization Code + PKCE.
No client secret required — suitable for human operators.

LumenRadio employees can log in with no arguments (client ID and tenant ID are baked in):

```bash
serial login
```

If the browser does not open automatically the auth URL is printed to the terminal so you
can paste it manually.

#### Machine-to-machine (M2M) login

Uses the OAuth 2.0 client credentials grant — suitable for automated pipelines.

```bash
serial login --mode m2m \
  --client-id <CLIENT_ID> \
  --client-secret <CLIENT_SECRET> \
  --tenant-id <TENANT_ID>
```

#### Common options

- `--mode [user|m2m]` — authentication mode (default: `user`) [env: SERIAL_AUTH_MODE]
- `--client-id` — Azure AD application client ID [env: SERIAL_CLIENT_ID]
- `--client-secret` — client secret, required for `m2m` [env: SERIAL_CLIENT_SECRET]
- `--tenant-id` — Azure AD tenant ID [env: SERIAL_TENANT_ID]
- `--token-url` — token endpoint URL override (m2m only) [env: SERIAL_TOKEN_URL]
- `--scope` — OAuth2 scope override [env: SERIAL_SCOPE]
- `--config-path` — override the login.json location

Stores the bearer token (and refresh token for user logins) in your config directory for
subsequent CLI commands.

### Status Command

Check backend reachability and token validity:

```bash
serial status
```

Reports:
- Backend reachable ✓ / unreachable ✗
- Token valid ✓ / invalid ✗

---

## Global Options

Most commands support:
- `--output [rich|json]` — output format (rich table default, json for scripting)
- `--config-path PATH` — override the login.json location

---

## Article Commands

### List Articles

```bash
serial list-articles
serial list-articles --output json
```

### Get Article

```bash
serial article get ARTICLE_NO
serial article get 710-4130 --output json
```

Returns full article details: article number, description, prefix, schema (version, fields, format, regexp), disabled flag.

### Create Article

```bash
serial article create \
  --article PROD-01 \
  --description "Production line 1" \
  --prefix PROD \
  --schema-file schema.json
```

The `--schema-file` must be a JSON file with the ArticleSchema object:

```json
{
  "version": 1,
  "fields": { "site": "string" },
  "format": "{prefix}-{site}-{counter:06d}",
  "regexp": "^[A-Z0-9]+-[A-Z0-9]+-[0-9]{6}$"
}
```

Options:
- `--article TEXT` — article number (1-32 chars, [A-Za-z0-9-]) [required]
- `--description TEXT` — human-readable description (1-100 chars) [required]
- `--prefix TEXT` — serial prefix (1-10 chars, [A-Za-z0-9]) [required]
- `--schema-file PATH` — path to JSON file containing the ArticleSchema [required]
- `--disabled / --no-disabled` — create as disabled (default: enabled)

### Update Article

```bash
serial article update ARTICLE_NO --description "New description"
serial article update ARTICLE_NO --prefix NEWP --disabled
```

At least one option must be provided. Options: `--description`, `--prefix`, `--disabled/--no-disabled`, `--schema-file`.

### Delete Article

```bash
serial article delete ARTICLE_NO
```

Idempotent — succeeds even if the article does not exist.

### Set Sequence Counter

```bash
serial article set-sequence ARTICLE_NO SEQUENCE_NO
serial article set-sequence 710-4130 1000
```

Sets the current counter value (integer ≥ 0) for the article.

---

## Serial Number Commands

### Generate Serial Numbers

```bash
serial generate ARTICLE_NO
serial generate ARTICLE_NO --quantity 5
serial generate ARTICLE_NO --field site=Stockholm --field year=2024
serial generate ARTICLE_NO --quantity 3 --output json
```

Options:
- `--quantity INT` — number of serials to generate (1-1000, default 1)
- `--field KEY=VALUE` — extra field value consumed by the article schema (repeatable)

### Lookup a Serial Number

```bash
serial lookup SERIAL_NO
serial lookup QWDNA010000001 --output json
```

Returns: serial value, article, created_by, created_at.

---

## Site Commands

### List Sites

```bash
serial site list
serial site list --output json
```

### Create Site

```bash
serial site create --site-no 5 --name "Helsinki"
```

Options:
- `--site-no INT` — site number (1-99) [required]
- `--name TEXT` — site name [required]

### Update Site

```bash
serial site update 5 --name "Helsinki" --contact-email ops@example.com
serial site update 5 --name "Helsinki" --disabled
```

Note: `--name` is required by the API even when only updating other fields.

Options:
- `--name TEXT` — new site name [required]
- `--client-id TEXT` — associated client ID (or empty to clear)
- `--contact-email TEXT` — contact email address (or empty to clear)
- `--disabled / --no-disabled` — enable or disable the site

### Delete Site

```bash
serial site delete 5
```

Idempotent — succeeds even if the site does not exist.

---

## Audit Commands

These commands require the `app.admin.api` scope.

### List Audit Entries

```bash
serial audit list
serial audit list --limit 50 --output json
```

Returns audit entries newest-first: id, touched_table, touched_key, change_type (create/update/delete), changed_by, changed_at.

Options:
- `--limit INT` — max entries to return (1-500, default 100)

### Restore an Audit Entry

```bash
serial audit restore AUDIT_ID
```

Undoes the audited change:
- `create` → deletes the created row
- `update` → restores previous values
- `delete` → re-inserts the deleted row

If the token lacks the admin scope, the API returns 403 and the CLI will display:
`Access denied. Ensure your credentials include the admin scope (app.admin.api).`
