Skip to content

Commit b1b5d50

Browse files
authored
Merge pull request #46 from akhundMurad/feature/explain
Feature: typeid explain — Human-Readable Explanations from ID Schemas
2 parents d9cd00b + fe128f6 commit b1b5d50

25 files changed

Lines changed: 2415 additions & 3 deletions

README.md

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ This particular implementation provides an pip package that can be used by any P
3636
poetry add typeid-python
3737
```
3838

39+
### Optional dependencies
40+
41+
TypeID supports schema-based ID explanations using JSON (always available) and
42+
YAML (optional).
43+
44+
To enable YAML support:
45+
46+
```console
47+
pip install typeid-python[yaml]
48+
```
49+
50+
If the extra is not installed, JSON schemas will still work.
51+
3952
## Usage
4053

4154
### Basic
@@ -109,3 +122,158 @@ This particular implementation provides an pip package that can be used by any P
109122
$ typeid encode 0188bac7-4afa-78aa-bc3b-bd1eef28d881 --prefix prefix
110123
prefix_01h2xcejqtf2nbrexx3vqjhp41
111124
```
125+
126+
## ✨ NEW: `typeid explain` — “What is this ID?”
127+
128+
TypeID can now **explain a TypeID** in a human-readable way.
129+
130+
This is useful when:
131+
132+
* debugging logs
133+
* inspecting database records
134+
* reviewing production incidents
135+
* understanding IDs shared via Slack, tickets, or dashboards
136+
137+
### Basic usage (no schema required)
138+
139+
```console
140+
$ typeid explain user_01h45ytscbebyvny4gc8cr8ma2
141+
```
142+
143+
Example output:
144+
145+
```yaml
146+
id: user_01h45ytscbebyvny4gc8cr8ma2
147+
valid: true
148+
149+
parsed:
150+
prefix: user
151+
suffix: 01h45ytscbebyvny4gc8cr8ma2
152+
uuid: 01890bf0-846f-7762-8605-5a3abb40e0e5
153+
created_at: 2025-03-12T10:41:23Z
154+
sortable: true
155+
156+
schema:
157+
found: false
158+
```
159+
160+
Even without configuration, `typeid explain` can:
161+
162+
* validate the ID
163+
* extract the UUID
164+
* derive creation time (UUIDv7)
165+
* determine sortability
166+
167+
## Schema-based explanations
168+
169+
To make explanations richer, you can define a **TypeID schema** describing what each
170+
prefix represents.
171+
172+
### Example schema (`typeid.schema.json`)
173+
174+
```json
175+
{
176+
"schema_version": 1,
177+
"types": {
178+
"user": {
179+
"name": "User",
180+
"description": "End-user account",
181+
"owner_team": "identity-platform",
182+
"pii": true,
183+
"retention": "7y",
184+
"links": {
185+
"logs": "https://logs.company/search?q={id}",
186+
"trace": "https://traces.company/?id={id}"
187+
}
188+
}
189+
}
190+
}
191+
```
192+
193+
### Explain using schema
194+
195+
```console
196+
$ typeid explain user_01h45ytscbebyvny4gc8cr8ma2
197+
```
198+
199+
Output (excerpt):
200+
201+
```yaml
202+
schema:
203+
found: true
204+
name: User
205+
owner_team: identity-platform
206+
pii: true
207+
retention: 7y
208+
209+
links:
210+
logs: https://logs.company/search?q=user_01h45ytscbebyvny4gc8cr8ma2
211+
```
212+
213+
## Schema discovery rules
214+
215+
If `--schema` is not provided, TypeID looks for a schema in the following order:
216+
217+
1. Environment variable:
218+
219+
```console
220+
TYPEID_SCHEMA=/path/to/schema.json
221+
```
222+
2. Current directory:
223+
224+
* `typeid.schema.json`
225+
* `typeid.schema.yaml`
226+
3. User config directory:
227+
228+
* `~/.config/typeid/schema.json`
229+
* `~/.config/typeid/schema.yaml`
230+
231+
If no schema is found, the command still works with derived information only.
232+
233+
## YAML schemas (optional)
234+
235+
YAML schemas are supported if the optional dependency is installed:
236+
237+
```console
238+
pip install typeid-python[yaml]
239+
```
240+
241+
Example (`typeid.schema.yaml`):
242+
243+
```yaml
244+
schema_version: 1
245+
types:
246+
user:
247+
name: User
248+
owner_team: identity-platform
249+
links:
250+
logs: "https://logs.company/search?q={id}"
251+
```
252+
253+
## JSON output (machine-readable)
254+
255+
```console
256+
$ typeid explain user_01h45ytscbebyvny4gc8cr8ma2 --json
257+
```
258+
259+
Useful for:
260+
261+
* scripts
262+
* CI pipelines
263+
* IDE integrations
264+
265+
## Design principles
266+
267+
* **Non-breaking**: existing APIs and CLI commands remain unchanged
268+
* **Schema-optional**: works fully offline
269+
* **Read-only**: no side effects or external mutations
270+
* **Declarative**: meaning is defined by users, not inferred by the tool
271+
272+
You can think of `typeid explain` as:
273+
274+
> **OpenAPI — but for identifiers instead of HTTP endpoints**
275+
276+
## License
277+
278+
MIT
279+

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()

0 commit comments

Comments
 (0)