Skip to content

Commit b200efc

Browse files
committed
feat: Add advanced usage examples for typeid explain
1 parent 72eab0c commit b200efc

7 files changed

Lines changed: 383 additions & 0 deletions

File tree

examples/README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# TypeID Examples
2+
3+
This directory contains **independent, self-contained examples** demonstrating
4+
different ways to use **TypeID in real projects**.
5+
6+
Each example focuses on a specific integration or use case and can be studied
7+
and used on its own.
8+
9+
## `examples/explain/``typeid explain` feature
10+
11+
This directory contains **advanced examples** for the `typeid explain` feature.
12+
13+
These examples demonstrate how to:
14+
15+
* inspect TypeIDs (“what is this ID?”)
16+
* enrich IDs using schemas (JSON / YAML)
17+
* batch-process IDs for automation
18+
* safely handle invalid or unknown IDs
19+
* generate machine-readable reports
20+
21+
📄 See **`examples/explain/README.md`** for full documentation and usage instructions.
22+
23+
## `examples/sqlalchemy.py` — SQLAlchemy integration
24+
25+
This example demonstrates how to use **TypeID with SQLAlchemy** in a clean and
26+
database-friendly way.
27+
28+
### Purpose
29+
30+
* Store **native UUIDs** in the database
31+
* Expose **TypeID objects** at the application level
32+
* Enforce prefix correctness automatically
33+
* Keep database schema simple and efficient
34+
35+
This example is **independent** of the `typeid explain` feature.
36+
37+
### What this example shows
38+
39+
* How to implement a custom `TypeDecorator` for TypeID
40+
* How to:
41+
42+
* bind a `TypeID` to a UUID column
43+
* reconstruct a `TypeID` on read
44+
* How to ensure:
45+
46+
* prefixes are validated
47+
* Alembic autogeneration preserves constructor arguments
48+
49+
### Usage snippet
50+
51+
```python
52+
id = mapped_column(
53+
TypeIDType("user"),
54+
primary_key=True,
55+
default=lambda: TypeID("user")
56+
)
57+
```
58+
59+
Resulting identifiers look like:
60+
61+
```text
62+
user_01h45ytscbebyvny4gc8cr8ma2
63+
```
64+
65+
while the database stores only the UUID value.
66+
67+
## Choosing the right example
68+
69+
| Use case | Example |
70+
| ---------------------------- | ------------------------------------ |
71+
| Understand `typeid explain` | `examples/explain/` |
72+
| Batch / CI / reporting | `examples/explain/explain_report.py` |
73+
| SQLAlchemy ORM integration | `examples/sqlalchemy.py` |
74+
| UUID-native database storage | `examples/sqlalchemy.py` |
75+
76+
## Design Principles
77+
78+
All examples in this directory follow these principles:
79+
80+
* ✅ non-breaking
81+
* ✅ production-oriented
82+
* ✅ minimal dependencies
83+
* ✅ explicit and readable
84+
* ✅ safe handling of invalid input
85+
86+
Examples are meant to be **copied, adapted, and extended**.

examples/explain/__init__.py

Whitespace-only changes.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
Complex example: schema discovery + taxonomy prefixes + robust handling.
3+
4+
Run:
5+
# (recommended) set schema location so discovery works
6+
export TYPEID_SCHEMA=examples/schemas/typeid.schema.json
7+
8+
python examples/explain_complex.py
9+
10+
Optional:
11+
pip install typeid-python[yaml]
12+
export TYPEID_SCHEMA=examples/schemas/typeid.schema.yaml
13+
"""
14+
15+
import os
16+
from typing import Iterable
17+
18+
from typeid import TypeID
19+
from typeid.explain.discovery import discover_schema_path
20+
from typeid.explain.registry import load_registry, make_lookup
21+
from typeid.explain.engine import explain as explain_engine
22+
from typeid.explain.formatters import format_explanation_pretty
23+
24+
25+
def _load_schema_lookup():
26+
discovery = discover_schema_path()
27+
if discovery.path is None:
28+
print("No schema discovered. Proceeding without schema.")
29+
return None
30+
31+
result = load_registry(discovery.path)
32+
if result.registry is None:
33+
print(f"Schema load failed: {result.error.message if result.error else 'unknown error'}")
34+
return None
35+
36+
print(f"Schema loaded from: {discovery.path} ({discovery.source})")
37+
return make_lookup(result.registry)
38+
39+
40+
def _banner(title: str) -> None:
41+
print("\n" + "=" * 80)
42+
print(title)
43+
print("=" * 80)
44+
45+
46+
def _explain_many(ids: Iterable[str], lookup) -> None:
47+
for tid in ids:
48+
exp = explain_engine(tid, schema_lookup=lookup, enable_schema=True, enable_links=True)
49+
print(format_explanation_pretty(exp))
50+
51+
52+
def main() -> None:
53+
_banner("TypeID explain — complex demo")
54+
55+
# Use schema discovery (env/cwd/user-config)
56+
lookup = _load_schema_lookup()
57+
58+
# Create a bunch of IDs:
59+
# - standard prefixes
60+
# - taxonomy prefix (env/region in prefix)
61+
# - unknown prefix
62+
# - invalid string
63+
user_id = str(TypeID(prefix="user"))
64+
order_id = str(TypeID(prefix="order"))
65+
evt_id = str(TypeID(prefix="evt_payment"))
66+
user_live_eu_id = str(TypeID(prefix="user_live_eu"))
67+
unknown_id = str(TypeID(prefix="something_new"))
68+
invalid_id = "user_NOT_A_SUFFIX"
69+
70+
_banner("Explaining generated IDs")
71+
ids = [user_id, order_id, evt_id, user_live_eu_id, unknown_id, invalid_id]
72+
_explain_many(ids, lookup)
73+
74+
_banner("Notes")
75+
print("- IDs still explain offline (derived facts always present).")
76+
print("- Schema adds meaning, ownership, policies, and links.")
77+
print("- Prefix taxonomy works because TypeID prefixes allow underscores.")
78+
print("- Invalid IDs never crash; they return valid=false and errors.")
79+
print("- Unknown prefixes still show derived facts, schema found=false.")
80+
81+
82+
if __name__ == "__main__":
83+
# Helpful hint for users
84+
if "TYPEID_SCHEMA" not in os.environ:
85+
print("Tip: set TYPEID_SCHEMA to enable schema discovery, e.g.:")
86+
print(" export TYPEID_SCHEMA=examples/schemas/typeid.schema.json\n")
87+
main()

examples/explain/explain_report.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
Batch report example:
3+
- Reads TypeIDs from a file (sample_ids.txt)
4+
- Explains each one
5+
- Prints summary stats
6+
- Optionally writes JSON report
7+
8+
Run:
9+
export TYPEID_SCHEMA=examples/schemas/typeid.schema.json
10+
python examples/explain_report.py examples/sample_ids.txt --json-out /tmp/report.json
11+
"""
12+
13+
import argparse
14+
import json
15+
from pathlib import Path
16+
17+
from typeid.explain.discovery import discover_schema_path
18+
from typeid.explain.engine import explain as explain_engine
19+
from typeid.explain.registry import load_registry, make_lookup
20+
21+
22+
def _read_ids(path: Path) -> list[str]:
23+
ids: list[str] = []
24+
for line in path.read_text(encoding="utf-8").splitlines():
25+
line = line.strip()
26+
if not line or line.startswith("#"):
27+
continue
28+
ids.append(line)
29+
return ids
30+
31+
32+
def main() -> None:
33+
parser = argparse.ArgumentParser()
34+
parser.add_argument("file", type=str, help="Path to file with TypeIDs (one per line).")
35+
parser.add_argument("--json-out", type=str, default=None, help="Optional path to write JSON report.")
36+
args = parser.parse_args()
37+
38+
ids = _read_ids(Path(args.file))
39+
40+
# Discover schema (optional)
41+
discovery = discover_schema_path()
42+
lookup = None
43+
schema_info = {"found": False}
44+
45+
if discovery.path is not None:
46+
r = load_registry(discovery.path)
47+
if r.registry is not None:
48+
lookup = make_lookup(r.registry)
49+
schema_info = {"found": True, "path": str(discovery.path), "source": discovery.source}
50+
else:
51+
schema_info = {"found": False, "error": r.error.message if r.error else "unknown"}
52+
53+
explanations = []
54+
valid_count = 0
55+
schema_hit = 0
56+
57+
for tid in ids:
58+
exp = explain_engine(tid, schema_lookup=lookup, enable_schema=True, enable_links=True)
59+
explanations.append(exp)
60+
if exp.valid:
61+
valid_count += 1
62+
if exp.schema is not None:
63+
schema_hit += 1
64+
65+
# Summary
66+
print("TypeID explain report")
67+
print("--------------------")
68+
print(f"IDs processed: {len(ids)}")
69+
print(f"Valid IDs: {valid_count}")
70+
print(f"Schema hits: {schema_hit}")
71+
print(f"Schema: {schema_info}")
72+
print()
73+
74+
# Print concise table
75+
for exp in explanations:
76+
prefix = exp.parsed.prefix or "-"
77+
ok = "OK" if exp.valid else "ERR"
78+
name = exp.schema.name if exp.schema and exp.schema.name else "-"
79+
print(f"{ok:>3} {prefix:<16} {name:<22} {exp.id}")
80+
81+
# Optional JSON output
82+
if args.json_out:
83+
payload = {
84+
"summary": {
85+
"count": len(ids),
86+
"valid": valid_count,
87+
"schema_hits": schema_hit,
88+
"schema": schema_info,
89+
},
90+
"items": [e.to_dict() for e in explanations],
91+
}
92+
out_path = Path(args.json_out)
93+
out_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
94+
print(f"\nWrote JSON report to: {out_path}")
95+
96+
97+
if __name__ == "__main__":
98+
main()

examples/explain/sample_ids.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Valid
2+
user_01h45ytscbebyvny4gc8cr8ma2
3+
order_01h2xcejqtf2nbrexx3vqjhp41
4+
5+
# Unknown prefix (still valid TypeID)
6+
mystery_01h2xcejqtf2nbrexx3vqjhp41
7+
8+
# Invalid
9+
user_NOT_A_SUFFIX
10+
not_a_typeid
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"schema_version": 1,
3+
"types": {
4+
"user": {
5+
"name": "User",
6+
"description": "End-user account",
7+
"owner_team": "identity-platform",
8+
"pii": true,
9+
"retention": "7y",
10+
"services": ["user-service", "auth-service"],
11+
"storage": {
12+
"primary": { "kind": "postgres", "table": "users", "shard_by": "tenant_id" }
13+
},
14+
"events": ["user.created", "user.updated", "user.deleted"],
15+
"policies": {
16+
"delete": { "allowed": false, "reason": "GDPR retention policy" }
17+
},
18+
"links": {
19+
"docs": "https://docs.company/entities/user",
20+
"logs": "https://logs.company/search?q={id}",
21+
"trace": "https://traces.company/?q={id}",
22+
"admin": "https://admin.company/users/{id}"
23+
}
24+
},
25+
26+
"order": {
27+
"name": "Order",
28+
"description": "Customer purchase order",
29+
"owner_team": "commerce-platform",
30+
"pii": false,
31+
"retention": "10y",
32+
"services": ["order-service", "billing-service"],
33+
"storage": {
34+
"primary": { "kind": "postgres", "table": "orders", "shard_by": "region" }
35+
},
36+
"events": ["order.created", "order.paid", "order.refunded"],
37+
"policies": {
38+
"delete": { "allowed": true, "reason": "No compliance hold" }
39+
},
40+
"links": {
41+
"admin": "https://admin.company/orders/{id}",
42+
"logs": "https://logs.company/search?q={id}"
43+
}
44+
},
45+
46+
"evt_payment": {
47+
"name": "PaymentEvent",
48+
"description": "Event emitted by payment pipeline",
49+
"owner_team": "payments",
50+
"pii": false,
51+
"retention": "30d",
52+
"services": ["payment-service"],
53+
"events": ["payment.authorized", "payment.failed", "payment.captured"],
54+
"policies": {
55+
"replay": { "allowed": false, "reason": "Non-idempotent event stream" }
56+
},
57+
"links": {
58+
"kafka": "https://kafka-ui.company/topics/payment-events?key={id}",
59+
"trace": "https://traces.company/?q={id}"
60+
}
61+
},
62+
63+
"user_live_eu": {
64+
"name": "User (prod EU)",
65+
"description": "Production EU user identifier (prefix taxonomy demo)",
66+
"owner_team": "identity-platform",
67+
"pii": true,
68+
"retention": "7y",
69+
"policies": {
70+
"cross_region": { "allowed": false, "reason": "EU PII must not leave EU" }
71+
},
72+
"links": {
73+
"logs": "https://logs.company/search?q={id}&region=eu",
74+
"admin": "https://admin.company/eu/users/{id}"
75+
}
76+
}
77+
}
78+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
schema_version: 1
2+
types:
3+
user:
4+
name: User
5+
description: End-user account
6+
owner_team: identity-platform
7+
pii: true
8+
retention: 7y
9+
services: [user-service, auth-service]
10+
storage:
11+
primary:
12+
kind: postgres
13+
table: users
14+
shard_by: tenant_id
15+
events: [user.created, user.updated, user.deleted]
16+
policies:
17+
delete:
18+
allowed: false
19+
reason: GDPR retention policy
20+
links:
21+
docs: "https://docs.company/entities/user"
22+
logs: "https://logs.company/search?q={id}"
23+
trace: "https://traces.company/?q={id}"
24+
admin: "https://admin.company/users/{id}"

0 commit comments

Comments
 (0)