Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ This particular implementation provides an pip package that can be used by any P
poetry add typeid-python
```

### Optional dependencies

TypeID supports schema-based ID explanations using JSON (always available) and
YAML (optional).

To enable YAML support:

```console
pip install typeid-python[yaml]
```

If the extra is not installed, JSON schemas will still work.

## Usage

### Basic
Expand Down Expand Up @@ -109,3 +122,158 @@ This particular implementation provides an pip package that can be used by any P
$ typeid encode 0188bac7-4afa-78aa-bc3b-bd1eef28d881 --prefix prefix
prefix_01h2xcejqtf2nbrexx3vqjhp41
```

## ✨ NEW: `typeid explain` — “What is this ID?”

TypeID can now **explain a TypeID** in a human-readable way.

This is useful when:

* debugging logs
* inspecting database records
* reviewing production incidents
* understanding IDs shared via Slack, tickets, or dashboards

### Basic usage (no schema required)

```console
$ typeid explain user_01h45ytscbebyvny4gc8cr8ma2
```

Example output:

```yaml
id: user_01h45ytscbebyvny4gc8cr8ma2
valid: true

parsed:
prefix: user
suffix: 01h45ytscbebyvny4gc8cr8ma2
uuid: 01890bf0-846f-7762-8605-5a3abb40e0e5
created_at: 2025-03-12T10:41:23Z
sortable: true

schema:
found: false
```

Even without configuration, `typeid explain` can:

* validate the ID
* extract the UUID
* derive creation time (UUIDv7)
* determine sortability

## Schema-based explanations

To make explanations richer, you can define a **TypeID schema** describing what each
prefix represents.

### Example schema (`typeid.schema.json`)

```json
{
"schema_version": 1,
"types": {
"user": {
"name": "User",
"description": "End-user account",
"owner_team": "identity-platform",
"pii": true,
"retention": "7y",
"links": {
"logs": "https://logs.company/search?q={id}",
"trace": "https://traces.company/?id={id}"
}
}
}
}
```

### Explain using schema

```console
$ typeid explain user_01h45ytscbebyvny4gc8cr8ma2
```

Output (excerpt):

```yaml
schema:
found: true
name: User
owner_team: identity-platform
pii: true
retention: 7y

links:
logs: https://logs.company/search?q=user_01h45ytscbebyvny4gc8cr8ma2
```

## Schema discovery rules

If `--schema` is not provided, TypeID looks for a schema in the following order:

1. Environment variable:

```console
TYPEID_SCHEMA=/path/to/schema.json
```
2. Current directory:

* `typeid.schema.json`
* `typeid.schema.yaml`
3. User config directory:

* `~/.config/typeid/schema.json`
* `~/.config/typeid/schema.yaml`

If no schema is found, the command still works with derived information only.

## YAML schemas (optional)

YAML schemas are supported if the optional dependency is installed:

```console
pip install typeid-python[yaml]
```

Example (`typeid.schema.yaml`):

```yaml
schema_version: 1
types:
user:
name: User
owner_team: identity-platform
links:
logs: "https://logs.company/search?q={id}"
```

## JSON output (machine-readable)

```console
$ typeid explain user_01h45ytscbebyvny4gc8cr8ma2 --json
```

Useful for:

* scripts
* CI pipelines
* IDE integrations

## Design principles

* **Non-breaking**: existing APIs and CLI commands remain unchanged
* **Schema-optional**: works fully offline
* **Read-only**: no side effects or external mutations
* **Declarative**: meaning is defined by users, not inferred by the tool

You can think of `typeid explain` as:

> **OpenAPI — but for identifiers instead of HTTP endpoints**

## License

MIT

86 changes: 86 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# TypeID Examples

This directory contains **independent, self-contained examples** demonstrating
different ways to use **TypeID in real projects**.

Each example focuses on a specific integration or use case and can be studied
and used on its own.

## `examples/explain/` — `typeid explain` feature

This directory contains **advanced examples** for the `typeid explain` feature.

These examples demonstrate how to:

* inspect TypeIDs (“what is this ID?”)
* enrich IDs using schemas (JSON / YAML)
* batch-process IDs for automation
* safely handle invalid or unknown IDs
* generate machine-readable reports

📄 See **`examples/explain/README.md`** for full documentation and usage instructions.

## `examples/sqlalchemy.py` — SQLAlchemy integration

This example demonstrates how to use **TypeID with SQLAlchemy** in a clean and
database-friendly way.

### Purpose

* Store **native UUIDs** in the database
* Expose **TypeID objects** at the application level
* Enforce prefix correctness automatically
* Keep database schema simple and efficient

This example is **independent** of the `typeid explain` feature.

### What this example shows

* How to implement a custom `TypeDecorator` for TypeID
* How to:

* bind a `TypeID` to a UUID column
* reconstruct a `TypeID` on read
* How to ensure:

* prefixes are validated
* Alembic autogeneration preserves constructor arguments

### Usage snippet

```python
id = mapped_column(
TypeIDType("user"),
primary_key=True,
default=lambda: TypeID("user")
)
```

Resulting identifiers look like:

```text
user_01h45ytscbebyvny4gc8cr8ma2
```

while the database stores only the UUID value.

## Choosing the right example

| Use case | Example |
| ---------------------------- | ------------------------------------ |
| Understand `typeid explain` | `examples/explain/` |
| Batch / CI / reporting | `examples/explain/explain_report.py` |
| SQLAlchemy ORM integration | `examples/sqlalchemy.py` |
| UUID-native database storage | `examples/sqlalchemy.py` |

## Design Principles

All examples in this directory follow these principles:

* ✅ non-breaking
* ✅ production-oriented
* ✅ minimal dependencies
* ✅ explicit and readable
* ✅ safe handling of invalid input

Examples are meant to be **copied, adapted, and extended**.
Empty file added examples/explain/__init__.py
Empty file.
87 changes: 87 additions & 0 deletions examples/explain/explain_complex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
Complex example: schema discovery + taxonomy prefixes + robust handling.

Run:
# (recommended) set schema location so discovery works
export TYPEID_SCHEMA=examples/schemas/typeid.schema.json

python examples/explain_complex.py

Optional:
pip install typeid-python[yaml]
export TYPEID_SCHEMA=examples/schemas/typeid.schema.yaml
"""

import os
from typing import Iterable

from typeid import TypeID
from typeid.explain.discovery import discover_schema_path
from typeid.explain.registry import load_registry, make_lookup
from typeid.explain.engine import explain as explain_engine
from typeid.explain.formatters import format_explanation_pretty


def _load_schema_lookup():
discovery = discover_schema_path()
if discovery.path is None:
print("No schema discovered. Proceeding without schema.")
return None

result = load_registry(discovery.path)
if result.registry is None:
print(f"Schema load failed: {result.error.message if result.error else 'unknown error'}")
return None

print(f"Schema loaded from: {discovery.path} ({discovery.source})")
return make_lookup(result.registry)


def _banner(title: str) -> None:
print("\n" + "=" * 80)
print(title)
print("=" * 80)


def _explain_many(ids: Iterable[str], lookup) -> None:
for tid in ids:
exp = explain_engine(tid, schema_lookup=lookup, enable_schema=True, enable_links=True)
print(format_explanation_pretty(exp))


def main() -> None:
_banner("TypeID explain — complex demo")

# Use schema discovery (env/cwd/user-config)
lookup = _load_schema_lookup()

# Create a bunch of IDs:
# - standard prefixes
# - taxonomy prefix (env/region in prefix)
# - unknown prefix
# - invalid string
user_id = str(TypeID(prefix="user"))
order_id = str(TypeID(prefix="order"))
evt_id = str(TypeID(prefix="evt_payment"))
user_live_eu_id = str(TypeID(prefix="user_live_eu"))
unknown_id = str(TypeID(prefix="something_new"))
invalid_id = "user_NOT_A_SUFFIX"

_banner("Explaining generated IDs")
ids = [user_id, order_id, evt_id, user_live_eu_id, unknown_id, invalid_id]
_explain_many(ids, lookup)

_banner("Notes")
print("- IDs still explain offline (derived facts always present).")
print("- Schema adds meaning, ownership, policies, and links.")
print("- Prefix taxonomy works because TypeID prefixes allow underscores.")
print("- Invalid IDs never crash; they return valid=false and errors.")
print("- Unknown prefixes still show derived facts, schema found=false.")


if __name__ == "__main__":
# Helpful hint for users
if "TYPEID_SCHEMA" not in os.environ:
print("Tip: set TYPEID_SCHEMA to enable schema discovery, e.g.:")
print(" export TYPEID_SCHEMA=examples/schemas/typeid.schema.json\n")
main()
Loading