typeid-python
Advanced tools
| import os | ||
| import uuid as std_uuid | ||
| from dataclasses import dataclass | ||
| from typing import Callable, Literal, Type | ||
| BackendName = Literal["uuid-utils", "uuid6"] | ||
| @dataclass(frozen=True) | ||
| class UUIDBackend: | ||
| name: BackendName | ||
| uuid7: Callable[[], std_uuid.UUID] | ||
| UUID: Type[std_uuid.UUID] | ||
| def _load_uuid_utils() -> UUIDBackend: | ||
| import uuid_utils as uuid # type: ignore | ||
| return UUIDBackend(name="uuid-utils", uuid7=uuid.uuid7, UUID=uuid.UUID) # type: ignore | ||
| def _load_uuid6() -> UUIDBackend: | ||
| import uuid6 # type: ignore | ||
| return UUIDBackend(name="uuid6", uuid7=uuid6.uuid7, UUID=uuid6.UUID) # type: ignore | ||
| def get_uuid_backend() -> UUIDBackend: | ||
| """ | ||
| Select UUIDv7 backend. | ||
| Selection order: | ||
| 1) If TYPEID_UUID_BACKEND is set, force that backend (or fail with a clear error). | ||
| 2) Otherwise prefer uuid-utils if installed, else fallback to uuid6. | ||
| Allowed values: | ||
| TYPEID_UUID_BACKEND=uuid-utils|uuid6 | ||
| """ | ||
| forced = os.getenv("TYPEID_UUID_BACKEND") | ||
| if forced: | ||
| forced = forced.strip() | ||
| if forced not in ("uuid-utils", "uuid6"): | ||
| raise RuntimeError(f"Invalid TYPEID_UUID_BACKEND={forced!r}. " "Allowed values: 'uuid-utils' or 'uuid6'.") | ||
| try: | ||
| return _load_uuid_utils() if forced == "uuid-utils" else _load_uuid6() | ||
| except Exception as e: | ||
| raise RuntimeError( | ||
| f"TYPEID_UUID_BACKEND is set to {forced!r}, but that backend " | ||
| "is not available. Install the required dependency." | ||
| ) from e | ||
| # Auto mode | ||
| try: | ||
| return _load_uuid_utils() | ||
| except Exception: | ||
| pass | ||
| try: | ||
| return _load_uuid6() | ||
| except Exception as e: | ||
| raise RuntimeError("No UUIDv7 backend available. Install one of: uuid-utils (recommended) or uuid6.") from e |
+123
-218
| Metadata-Version: 2.4 | ||
| Name: typeid-python | ||
| Version: 0.3.4 | ||
| Version: 0.3.5 | ||
| Summary: Python implementation of TypeIDs: type-safe, K-sortable, and globally unique identifiers inspired by Stripe IDs | ||
@@ -24,2 +24,4 @@ Project-URL: Homepage, https://github.com/akhundMurad/typeid-python | ||
| Requires-Dist: click; extra == 'cli' | ||
| Provides-Extra: rust | ||
| Requires-Dist: uuid-utils>=0.12.0; extra == 'rust' | ||
| Provides-Extra: yaml | ||
@@ -31,174 +33,161 @@ Requires-Dist: pyyaml; extra == 'yaml' | ||
| <a href="https://github.com/akhundMurad/typeid-python/actions?query=setup%3ACI%2FCD+event%3Apush+branch%3Amain" target="_blank"> | ||
| <img src="https://github.com/akhundMurad/typeid-python/actions/workflows/setup.yml/badge.svg?event=push&branch=main" alt="Test"> | ||
| </a> | ||
| <a href="https://pepy.tech/project/typeid-python" target="_blank"> | ||
| <img src="https://static.pepy.tech/personalized-badge/typeid-python?period=total&units=international_system&left_color=black&right_color=red&left_text=downloads" alt="Downloads"> | ||
| </a> | ||
| <a href="https://pypi.org/project/typeid-python" target="_blank"> | ||
| <img src="https://img.shields.io/pypi/v/typeid-python?color=red&labelColor=black" alt="Package version"> | ||
| </a> | ||
| <a href="https://pypi.org/project/typeid-python" target="_blank"> | ||
| <img src="https://img.shields.io/pypi/pyversions/typeid-python.svg?color=red&labelColor=black" alt="Supported Python versions"> | ||
| </a> | ||
| [](https://github.com/akhundMurad/typeid-python/actions/workflows/test.yml) | ||
| [](https://pepy.tech/projects/typeid-python) | ||
| [](https://pypi.org/project/typeid-python/) | ||
| [](https://pypi.org/project/typeid-python/) | ||
| ## A Python implementation of [TypeIDs](https://github.com/jetpack-io/typeid) using Python | ||
| TypeIDs are a modern, **type-safe**, globally unique identifier based on the upcoming | ||
| UUIDv7 standard. They provide a ton of nice properties that make them a great choice | ||
| as the primary identifiers for your data in a database, APIs, and distributed systems. | ||
| Read more about TypeIDs in their [spec](https://github.com/jetpack-io/typeid). | ||
| A **high-performance Python implementation of [TypeIDs](https://github.com/jetpack-io/typeid)** — type-safe, | ||
| sortable identifiers based on **UUIDv7**. | ||
| This particular implementation provides an pip package that can be used by any Python project. | ||
| TypeIDs are designed for modern systems where identifiers should be: | ||
| ## Installation | ||
| - globally unique | ||
| - sortable by creation time | ||
| - safe to expose externally | ||
| - easy to reason about in logs, APIs, and databases | ||
| - Pip: | ||
| This library provides a Python package with optional Rust acceleration. | ||
| ```console | ||
| pip install typeid-python | ||
| ``` | ||
| ## Key features | ||
| - Uv: | ||
| - ✅ UUIDv7-based, time-sortable identifiers | ||
| - ✅ Type-safe prefixes (`user_`, `order_`, …) | ||
| - ✅ Human-readable and URL-safe | ||
| - ✅ Fast generation & parsing (Rust-accelerated) | ||
| - ✅ CLI tools (`new`, `encode`, `decode`, `explain`) | ||
| - ✅ Schema-based ID explanations (JSON / YAML) | ||
| - ✅ Fully offline, no external services | ||
| ```console | ||
| uv add typeid-python | ||
| ``` | ||
| ## Performance | ||
| - Poetry: | ||
| TypeID is optimized for **real-world performance**, not just correctness. | ||
| ```console | ||
| poetry add typeid-python | ||
| ``` | ||
| ### Benchmark summary (mean time) | ||
| ### Optional dependencies | ||
| | Operation | Before Rust | Rust + optimizations | | ||
| | --------- | ----------- | -------------------- | | ||
| | Generate | 3.47 µs | **0.70 µs** | | ||
| | Parse | 2.08 µs | **1.30 µs** | | ||
| | Workflow | 5.52 µs | **2.25 µs** | | ||
| TypeID supports schema-based ID explanations using JSON (always available) and | ||
| YAML (optional). | ||
| ### Highlights | ||
| To enable YAML support: | ||
| * 🚀 **~5× faster generation** | ||
| * ⚡ **~1.6× faster parsing** | ||
| * 🔁 **~2.5× faster end-to-end workflows** | ||
| ```console | ||
| pip install typeid-python[yaml] | ||
| ``` | ||
| Benchmarks are: | ||
| If the extra is not installed, JSON schemas will still work. | ||
| * reproducible | ||
| * committed as raw JSON | ||
| * runnable locally via `bench/` | ||
| ## Usage | ||
| See [`Docs: Performance`](https://akhundmurad.github.io/typeid-python/performance/) for details. | ||
| ### Basic | ||
| ## Installation | ||
| - Create TypeID Instance: | ||
| ### Core (pure Python) | ||
| ```python | ||
| from typeid import TypeID | ||
| ```console | ||
| $ pip install typeid-python | ||
| ``` | ||
| # Default TypeID (no prefix) | ||
| typeid = TypeID() | ||
| ### With Rust acceleration (recommended) | ||
| assert typeid.prefix == "" | ||
| assert isinstance(typeid.suffix, str) | ||
| assert len(typeid.suffix) > 0 # encoded UUIDv7 | ||
| ```console | ||
| $ pip install typeid-python[rust] | ||
| ``` | ||
| # TypeID with prefix | ||
| typeid = TypeID(prefix="user") | ||
| This enables: | ||
| assert typeid.prefix == "user" | ||
| assert str(typeid).startswith("user_") | ||
| ``` | ||
| * Rust base32 encode/decode | ||
| * `uuid-utils` for fast UUIDv7 generation | ||
| - Create TypeID from string: | ||
| If Rust is unavailable, TypeID automatically falls back to the pure-Python implementation. | ||
| ```python | ||
| from typeid import TypeID | ||
| ### Other optional extras | ||
| value = "user_01h45ytscbebyvny4gc8cr8ma2" | ||
| typeid = TypeID.from_string(value) | ||
| ```console | ||
| $ pip install typeid-python[yaml] # YAML schema support | ||
| $ pip install typeid-python[cli] # CLI tools | ||
| ``` | ||
| assert str(typeid) == value | ||
| assert typeid.prefix == "user" | ||
| ``` | ||
| Extras are **strictly optional**. | ||
| - Create TypeID from uuid7: | ||
| ## Usage | ||
| ```python | ||
| from typeid import TypeID | ||
| from uuid6 import uuid7 | ||
| ### Basic | ||
| uuid = uuid7() | ||
| prefix = "user" | ||
| ```python | ||
| from typeid import TypeID | ||
| typeid = TypeID.from_uuid(prefix=prefix, suffix=uuid) | ||
| tid = TypeID(prefix="user") | ||
| assert typeid.prefix == prefix | ||
| assert str(typeid).startswith(f"{prefix}_") | ||
| assert tid.prefix == "user" | ||
| assert isinstance(tid.suffix, str) | ||
| assert str(tid).startswith("user_") | ||
| ``` | ||
| ``` | ||
| ### From string | ||
| - Use pre-defined prefix: | ||
| ```python | ||
| from typeid import TypeID | ||
| ```python | ||
| from dataclasses import dataclass, field | ||
| from typing import Literal | ||
| from typeid import TypeID, typeid_factory | ||
| tid = TypeID.from_string("user_01h45ytscbebyvny4gc8cr8ma2") | ||
| assert tid.prefix == "user" | ||
| ``` | ||
| UserID = TypeID[Literal["user"]] | ||
| gen_user_id = typeid_factory("user") | ||
| ### From UUIDv7 | ||
| ```python | ||
| from typeid import TypeID | ||
| from uuid6 import uuid7 | ||
| @dataclass | ||
| class UserDTO: | ||
| user_id: UserID = field(default_factory=gen_user_id) | ||
| full_name: str = "A J" | ||
| age: int = 18 | ||
| u = uuid7() | ||
| tid = TypeID.from_uuid(prefix="user", suffix=u) | ||
| assert tid.uuid.version == 7 | ||
| ``` | ||
| user = UserDTO() | ||
| ### Typed prefixes | ||
| assert str(user.user_id).startswith("user_") | ||
| ``` | ||
| ```python | ||
| from typing import Literal | ||
| from typeid import TypeID, typeid_factory | ||
| ### CLI-tool | ||
| UserID = TypeID[Literal["user"]] | ||
| gen_user_id = typeid_factory("user") | ||
| - Install dependencies: | ||
| user_id = gen_user_id() | ||
| ``` | ||
| ```console | ||
| pip install typeid-python[cli] | ||
| ``` | ||
| ## CLI | ||
| - To generate a new TypeID, run: | ||
| ```console | ||
| $ pip install typeid-python[cli] | ||
| ``` | ||
| ```console | ||
| $ typeid new -p prefix | ||
| prefix_01h2xcejqtf2nbrexx3vqjhp41 | ||
| ``` | ||
| Generate: | ||
| - To decode an existing TypeID into a UUID run: | ||
| ```console | ||
| $ typeid new -p user | ||
| user_01h2xcejqtf2nbrexx3vqjhp41 | ||
| ``` | ||
| ```console | ||
| $ typeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41 | ||
| type: prefix | ||
| uuid: 0188bac7-4afa-78aa-bc3b-bd1eef28d881 | ||
| ``` | ||
| Decode: | ||
| - And to encode an existing UUID into a TypeID run: | ||
| ```console | ||
| $ typeid decode user_01h2xcejqtf2nbrexx3vqjhp41 | ||
| uuid: 0188bac7-4afa-78aa-bc3b-bd1eef28d881 | ||
| ``` | ||
| ```console | ||
| $ typeid encode 0188bac7-4afa-78aa-bc3b-bd1eef28d881 --prefix prefix | ||
| prefix_01h2xcejqtf2nbrexx3vqjhp41 | ||
| ``` | ||
| Encode: | ||
| ## ✨ NEW: `typeid explain` — “What is this ID?” | ||
| ```console | ||
| $ typeid encode 0188bac7-4afa-78aa-bc3b-bd1eef28d881 --prefix user | ||
| ``` | ||
| TypeID can now **explain a TypeID** in a human-readable way. | ||
| ## ✨ `typeid explain` — understand any ID | ||
| 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 | ||
@@ -208,32 +197,19 @@ $ typeid explain user_01h45ytscbebyvny4gc8cr8ma2 | ||
| Example output: | ||
| Outputs: | ||
| ```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: | ||
| Works **without schema**, fully offline. | ||
| * 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. | ||
| Define meaning for prefixes using JSON or YAML. | ||
| ### Example schema (`typeid.schema.json`) | ||
| Example (`typeid.schema.json`): | ||
@@ -246,10 +222,4 @@ ```json | ||
| "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}" | ||
| } | ||
| "pii": true | ||
| } | ||
@@ -260,3 +230,3 @@ } | ||
| ### Explain using schema | ||
| Then: | ||
@@ -267,82 +237,17 @@ ```console | ||
| Output (excerpt): | ||
| Read more here: ["Docs: Explain"](https://akhundmurad.github.io/typeid-python/performance/). | ||
| ```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 | ||
| * **Non-breaking**: stable APIs | ||
| * **Optional acceleration**: Rust is opt-in | ||
| * **Lazy evaluation**: work is done only when needed | ||
| * **Explainability**: identifiers carry meaning | ||
| * **Transparency**: performance claims are backed by data | ||
| You can think of `typeid explain` as: | ||
| > Think of TypeID as | ||
| > **UUIDs + semantics + observability — without sacrificing speed** | ||
| > **OpenAPI — but for identifiers instead of HTTP endpoints** | ||
| ## License | ||
| MIT | ||
+4
-1
| [project] | ||
| name = "typeid-python" | ||
| version = "0.3.4" | ||
| version = "0.3.5" | ||
| description = "Python implementation of TypeIDs: type-safe, K-sortable, and globally unique identifiers inspired by Stripe IDs" | ||
@@ -30,2 +30,3 @@ authors = [{ name = "Murad Akhundov", email = "akhundov1murad@gmail.com" }] | ||
| yaml = ["PyYAML"] | ||
| rust = ["uuid-utils>=0.12.0"] | ||
@@ -56,2 +57,4 @@ [project.urls] | ||
| "pytest-markdown-docs>=0.9.0", | ||
| "pytest-benchmark>=5.0.1", | ||
| "maturin>=1.5; platform_system != 'Windows'" | ||
| ] | ||
@@ -58,0 +61,0 @@ |
+120
-217
| # TypeID Python | ||
| <a href="https://github.com/akhundMurad/typeid-python/actions?query=setup%3ACI%2FCD+event%3Apush+branch%3Amain" target="_blank"> | ||
| <img src="https://github.com/akhundMurad/typeid-python/actions/workflows/setup.yml/badge.svg?event=push&branch=main" alt="Test"> | ||
| </a> | ||
| <a href="https://pepy.tech/project/typeid-python" target="_blank"> | ||
| <img src="https://static.pepy.tech/personalized-badge/typeid-python?period=total&units=international_system&left_color=black&right_color=red&left_text=downloads" alt="Downloads"> | ||
| </a> | ||
| <a href="https://pypi.org/project/typeid-python" target="_blank"> | ||
| <img src="https://img.shields.io/pypi/v/typeid-python?color=red&labelColor=black" alt="Package version"> | ||
| </a> | ||
| <a href="https://pypi.org/project/typeid-python" target="_blank"> | ||
| <img src="https://img.shields.io/pypi/pyversions/typeid-python.svg?color=red&labelColor=black" alt="Supported Python versions"> | ||
| </a> | ||
| [](https://github.com/akhundMurad/typeid-python/actions/workflows/test.yml) | ||
| [](https://pepy.tech/projects/typeid-python) | ||
| [](https://pypi.org/project/typeid-python/) | ||
| [](https://pypi.org/project/typeid-python/) | ||
| ## A Python implementation of [TypeIDs](https://github.com/jetpack-io/typeid) using Python | ||
| TypeIDs are a modern, **type-safe**, globally unique identifier based on the upcoming | ||
| UUIDv7 standard. They provide a ton of nice properties that make them a great choice | ||
| as the primary identifiers for your data in a database, APIs, and distributed systems. | ||
| Read more about TypeIDs in their [spec](https://github.com/jetpack-io/typeid). | ||
| A **high-performance Python implementation of [TypeIDs](https://github.com/jetpack-io/typeid)** — type-safe, | ||
| sortable identifiers based on **UUIDv7**. | ||
| This particular implementation provides an pip package that can be used by any Python project. | ||
| TypeIDs are designed for modern systems where identifiers should be: | ||
| ## Installation | ||
| - globally unique | ||
| - sortable by creation time | ||
| - safe to expose externally | ||
| - easy to reason about in logs, APIs, and databases | ||
| - Pip: | ||
| This library provides a Python package with optional Rust acceleration. | ||
| ```console | ||
| pip install typeid-python | ||
| ``` | ||
| ## Key features | ||
| - Uv: | ||
| - ✅ UUIDv7-based, time-sortable identifiers | ||
| - ✅ Type-safe prefixes (`user_`, `order_`, …) | ||
| - ✅ Human-readable and URL-safe | ||
| - ✅ Fast generation & parsing (Rust-accelerated) | ||
| - ✅ CLI tools (`new`, `encode`, `decode`, `explain`) | ||
| - ✅ Schema-based ID explanations (JSON / YAML) | ||
| - ✅ Fully offline, no external services | ||
| ```console | ||
| uv add typeid-python | ||
| ``` | ||
| ## Performance | ||
| - Poetry: | ||
| TypeID is optimized for **real-world performance**, not just correctness. | ||
| ```console | ||
| poetry add typeid-python | ||
| ``` | ||
| ### Benchmark summary (mean time) | ||
| ### Optional dependencies | ||
| | Operation | Before Rust | Rust + optimizations | | ||
| | --------- | ----------- | -------------------- | | ||
| | Generate | 3.47 µs | **0.70 µs** | | ||
| | Parse | 2.08 µs | **1.30 µs** | | ||
| | Workflow | 5.52 µs | **2.25 µs** | | ||
| TypeID supports schema-based ID explanations using JSON (always available) and | ||
| YAML (optional). | ||
| ### Highlights | ||
| To enable YAML support: | ||
| * 🚀 **~5× faster generation** | ||
| * ⚡ **~1.6× faster parsing** | ||
| * 🔁 **~2.5× faster end-to-end workflows** | ||
| ```console | ||
| pip install typeid-python[yaml] | ||
| ``` | ||
| Benchmarks are: | ||
| If the extra is not installed, JSON schemas will still work. | ||
| * reproducible | ||
| * committed as raw JSON | ||
| * runnable locally via `bench/` | ||
| ## Usage | ||
| See [`Docs: Performance`](https://akhundmurad.github.io/typeid-python/performance/) for details. | ||
| ### Basic | ||
| ## Installation | ||
| - Create TypeID Instance: | ||
| ### Core (pure Python) | ||
| ```python | ||
| from typeid import TypeID | ||
| ```console | ||
| $ pip install typeid-python | ||
| ``` | ||
| # Default TypeID (no prefix) | ||
| typeid = TypeID() | ||
| ### With Rust acceleration (recommended) | ||
| assert typeid.prefix == "" | ||
| assert isinstance(typeid.suffix, str) | ||
| assert len(typeid.suffix) > 0 # encoded UUIDv7 | ||
| ```console | ||
| $ pip install typeid-python[rust] | ||
| ``` | ||
| # TypeID with prefix | ||
| typeid = TypeID(prefix="user") | ||
| This enables: | ||
| assert typeid.prefix == "user" | ||
| assert str(typeid).startswith("user_") | ||
| ``` | ||
| * Rust base32 encode/decode | ||
| * `uuid-utils` for fast UUIDv7 generation | ||
| - Create TypeID from string: | ||
| If Rust is unavailable, TypeID automatically falls back to the pure-Python implementation. | ||
| ```python | ||
| from typeid import TypeID | ||
| ### Other optional extras | ||
| value = "user_01h45ytscbebyvny4gc8cr8ma2" | ||
| typeid = TypeID.from_string(value) | ||
| ```console | ||
| $ pip install typeid-python[yaml] # YAML schema support | ||
| $ pip install typeid-python[cli] # CLI tools | ||
| ``` | ||
| assert str(typeid) == value | ||
| assert typeid.prefix == "user" | ||
| ``` | ||
| Extras are **strictly optional**. | ||
| - Create TypeID from uuid7: | ||
| ## Usage | ||
| ```python | ||
| from typeid import TypeID | ||
| from uuid6 import uuid7 | ||
| ### Basic | ||
| uuid = uuid7() | ||
| prefix = "user" | ||
| ```python | ||
| from typeid import TypeID | ||
| typeid = TypeID.from_uuid(prefix=prefix, suffix=uuid) | ||
| tid = TypeID(prefix="user") | ||
| assert typeid.prefix == prefix | ||
| assert str(typeid).startswith(f"{prefix}_") | ||
| assert tid.prefix == "user" | ||
| assert isinstance(tid.suffix, str) | ||
| assert str(tid).startswith("user_") | ||
| ``` | ||
| ``` | ||
| ### From string | ||
| - Use pre-defined prefix: | ||
| ```python | ||
| from typeid import TypeID | ||
| ```python | ||
| from dataclasses import dataclass, field | ||
| from typing import Literal | ||
| from typeid import TypeID, typeid_factory | ||
| tid = TypeID.from_string("user_01h45ytscbebyvny4gc8cr8ma2") | ||
| assert tid.prefix == "user" | ||
| ``` | ||
| UserID = TypeID[Literal["user"]] | ||
| gen_user_id = typeid_factory("user") | ||
| ### From UUIDv7 | ||
| ```python | ||
| from typeid import TypeID | ||
| from uuid6 import uuid7 | ||
| @dataclass | ||
| class UserDTO: | ||
| user_id: UserID = field(default_factory=gen_user_id) | ||
| full_name: str = "A J" | ||
| age: int = 18 | ||
| u = uuid7() | ||
| tid = TypeID.from_uuid(prefix="user", suffix=u) | ||
| assert tid.uuid.version == 7 | ||
| ``` | ||
| user = UserDTO() | ||
| ### Typed prefixes | ||
| assert str(user.user_id).startswith("user_") | ||
| ``` | ||
| ```python | ||
| from typing import Literal | ||
| from typeid import TypeID, typeid_factory | ||
| ### CLI-tool | ||
| UserID = TypeID[Literal["user"]] | ||
| gen_user_id = typeid_factory("user") | ||
| - Install dependencies: | ||
| user_id = gen_user_id() | ||
| ``` | ||
| ```console | ||
| pip install typeid-python[cli] | ||
| ``` | ||
| ## CLI | ||
| - To generate a new TypeID, run: | ||
| ```console | ||
| $ pip install typeid-python[cli] | ||
| ``` | ||
| ```console | ||
| $ typeid new -p prefix | ||
| prefix_01h2xcejqtf2nbrexx3vqjhp41 | ||
| ``` | ||
| Generate: | ||
| - To decode an existing TypeID into a UUID run: | ||
| ```console | ||
| $ typeid new -p user | ||
| user_01h2xcejqtf2nbrexx3vqjhp41 | ||
| ``` | ||
| ```console | ||
| $ typeid decode prefix_01h2xcejqtf2nbrexx3vqjhp41 | ||
| type: prefix | ||
| uuid: 0188bac7-4afa-78aa-bc3b-bd1eef28d881 | ||
| ``` | ||
| Decode: | ||
| - And to encode an existing UUID into a TypeID run: | ||
| ```console | ||
| $ typeid decode user_01h2xcejqtf2nbrexx3vqjhp41 | ||
| uuid: 0188bac7-4afa-78aa-bc3b-bd1eef28d881 | ||
| ``` | ||
| ```console | ||
| $ typeid encode 0188bac7-4afa-78aa-bc3b-bd1eef28d881 --prefix prefix | ||
| prefix_01h2xcejqtf2nbrexx3vqjhp41 | ||
| ``` | ||
| Encode: | ||
| ## ✨ NEW: `typeid explain` — “What is this ID?” | ||
| ```console | ||
| $ typeid encode 0188bac7-4afa-78aa-bc3b-bd1eef28d881 --prefix user | ||
| ``` | ||
| TypeID can now **explain a TypeID** in a human-readable way. | ||
| ## ✨ `typeid explain` — understand any ID | ||
| 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 | ||
@@ -179,32 +166,19 @@ $ typeid explain user_01h45ytscbebyvny4gc8cr8ma2 | ||
| Example output: | ||
| Outputs: | ||
| ```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: | ||
| Works **without schema**, fully offline. | ||
| * 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. | ||
| Define meaning for prefixes using JSON or YAML. | ||
| ### Example schema (`typeid.schema.json`) | ||
| Example (`typeid.schema.json`): | ||
@@ -217,10 +191,4 @@ ```json | ||
| "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}" | ||
| } | ||
| "pii": true | ||
| } | ||
@@ -231,3 +199,3 @@ } | ||
| ### Explain using schema | ||
| Then: | ||
@@ -238,82 +206,17 @@ ```console | ||
| Output (excerpt): | ||
| Read more here: ["Docs: Explain"](https://akhundmurad.github.io/typeid-python/performance/). | ||
| ```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 | ||
| * **Non-breaking**: stable APIs | ||
| * **Optional acceleration**: Rust is opt-in | ||
| * **Lazy evaluation**: work is done only when needed | ||
| * **Explainability**: identifiers carry meaning | ||
| * **Transparency**: performance claims are backed by data | ||
| You can think of `typeid explain` as: | ||
| > Think of TypeID as | ||
| > **UUIDs + semantics + observability — without sacrificing speed** | ||
| > **OpenAPI — but for identifiers instead of HTTP endpoints** | ||
| ## License | ||
| MIT | ||
+87
-212
@@ -1,3 +0,11 @@ | ||
| from typing import List | ||
| try: | ||
| from typeid_base32 import encode as _encode_rust, decode as _decode_rust # type: ignore | ||
| _HAS_RUST = True | ||
| except Exception: | ||
| _HAS_RUST = False | ||
| _encode_rust = None | ||
| _decode_rust = None | ||
| from typing import Union | ||
| from typeid.constants import SUFFIX_LEN | ||
@@ -7,3 +15,3 @@ | ||
| # TABLE maps ASCII byte -> 0..31 or 0xFF if invalid | ||
| TABLE = [ | ||
@@ -138,227 +146,94 @@ 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| 0xFF, | ||
| ] | ||
| ] + [0xFF] * (256 - 128) | ||
| def encode(src: List[int]) -> str: | ||
| dst = [""] * SUFFIX_LEN | ||
| BytesLike = Union[bytes, bytearray, memoryview] | ||
| if len(src) != 16: | ||
| def _encode_py(src: BytesLike) -> str: | ||
| mv = memoryview(src) | ||
| if mv.nbytes != 16: | ||
| raise RuntimeError("Invalid length.") | ||
| # 10 byte timestamp | ||
| dst[0] = ALPHABET[(src[0] & 224) >> 5] | ||
| dst[1] = ALPHABET[src[0] & 31] | ||
| dst[2] = ALPHABET[(src[1] & 248) >> 3] | ||
| dst[3] = ALPHABET[((src[1] & 7) << 2) | ((src[2] & 192) >> 6)] | ||
| dst[4] = ALPHABET[(src[2] & 62) >> 1] | ||
| dst[5] = ALPHABET[((src[2] & 1) << 4) | ((src[3] & 240) >> 4)] | ||
| dst[6] = ALPHABET[((src[3] & 15) << 1) | ((src[4] & 128) >> 7)] | ||
| dst[7] = ALPHABET[(src[4] & 124) >> 2] | ||
| dst[8] = ALPHABET[((src[4] & 3) << 3) | ((src[5] & 224) >> 5)] | ||
| dst[9] = ALPHABET[src[5] & 31] | ||
| # Pre-allocate output chars | ||
| dst = [""] * SUFFIX_LEN | ||
| # 16 bytes of randomness | ||
| dst[10] = ALPHABET[(src[6] & 248) >> 3] | ||
| dst[11] = ALPHABET[((src[6] & 7) << 2) | ((src[7] & 192) >> 6)] | ||
| dst[12] = ALPHABET[(src[7] & 62) >> 1] | ||
| dst[13] = ALPHABET[((src[7] & 1) << 4) | ((src[8] & 240) >> 4)] | ||
| dst[14] = ALPHABET[((src[8] & 15) << 1) | ((src[9] & 128) >> 7)] | ||
| dst[15] = ALPHABET[(src[9] & 124) >> 2] | ||
| dst[16] = ALPHABET[((src[9] & 3) << 3) | ((src[10] & 224) >> 5)] | ||
| dst[17] = ALPHABET[src[10] & 31] | ||
| dst[18] = ALPHABET[(src[11] & 248) >> 3] | ||
| dst[19] = ALPHABET[((src[11] & 7) << 2) | ((src[12] & 192) >> 6)] | ||
| dst[20] = ALPHABET[(src[12] & 62) >> 1] | ||
| dst[21] = ALPHABET[((src[12] & 1) << 4) | ((src[13] & 240) >> 4)] | ||
| dst[22] = ALPHABET[((src[13] & 15) << 1) | ((src[14] & 128) >> 7)] | ||
| dst[23] = ALPHABET[(src[14] & 124) >> 2] | ||
| dst[24] = ALPHABET[((src[14] & 3) << 3) | ((src[15] & 224) >> 5)] | ||
| dst[25] = ALPHABET[src[15] & 31] | ||
| # Timestamp (6 bytes => 10 chars) | ||
| dst[0] = ALPHABET[(mv[0] & 0b11100000) >> 5] | ||
| dst[1] = ALPHABET[mv[0] & 0b00011111] | ||
| dst[2] = ALPHABET[(mv[1] & 0b11111000) >> 3] | ||
| dst[3] = ALPHABET[((mv[1] & 0b00000111) << 2) | ((mv[2] & 0b11000000) >> 6)] | ||
| dst[4] = ALPHABET[(mv[2] & 0b00111110) >> 1] | ||
| dst[5] = ALPHABET[((mv[2] & 0b00000001) << 4) | ((mv[3] & 0b11110000) >> 4)] | ||
| dst[6] = ALPHABET[((mv[3] & 0b00001111) << 1) | ((mv[4] & 0b10000000) >> 7)] | ||
| dst[7] = ALPHABET[(mv[4] & 0b01111100) >> 2] | ||
| dst[8] = ALPHABET[((mv[4] & 0b00000011) << 3) | ((mv[5] & 0b11100000) >> 5)] | ||
| dst[9] = ALPHABET[mv[5] & 0b00011111] | ||
| # Entropy (10 bytes => 16 chars) | ||
| dst[10] = ALPHABET[(mv[6] & 0b11111000) >> 3] | ||
| dst[11] = ALPHABET[((mv[6] & 0b00000111) << 2) | ((mv[7] & 0b11000000) >> 6)] | ||
| dst[12] = ALPHABET[(mv[7] & 0b00111110) >> 1] | ||
| dst[13] = ALPHABET[((mv[7] & 0b00000001) << 4) | ((mv[8] & 0b11110000) >> 4)] | ||
| dst[14] = ALPHABET[((mv[8] & 0b00001111) << 1) | ((mv[9] & 0b10000000) >> 7)] | ||
| dst[15] = ALPHABET[(mv[9] & 0b01111100) >> 2] | ||
| dst[16] = ALPHABET[((mv[9] & 0b00000011) << 3) | ((mv[10] & 0b11100000) >> 5)] | ||
| dst[17] = ALPHABET[mv[10] & 0b00011111] | ||
| dst[18] = ALPHABET[(mv[11] & 0b11111000) >> 3] | ||
| dst[19] = ALPHABET[((mv[11] & 0b00000111) << 2) | ((mv[12] & 0b11000000) >> 6)] | ||
| dst[20] = ALPHABET[(mv[12] & 0b00111110) >> 1] | ||
| dst[21] = ALPHABET[((mv[12] & 0b00000001) << 4) | ((mv[13] & 0b11110000) >> 4)] | ||
| dst[22] = ALPHABET[((mv[13] & 0b00001111) << 1) | ((mv[14] & 0b10000000) >> 7)] | ||
| dst[23] = ALPHABET[(mv[14] & 0b01111100) >> 2] | ||
| dst[24] = ALPHABET[((mv[14] & 0b00000011) << 3) | ((mv[15] & 0b11100000) >> 5)] | ||
| dst[25] = ALPHABET[mv[15] & 0b00011111] | ||
| return "".join(dst) | ||
| def decode(s: str) -> List[int]: | ||
| v = bytes(s, encoding="utf-8") | ||
| def _decode_py(s: str) -> bytes: | ||
| if len(s) != SUFFIX_LEN: | ||
| raise RuntimeError("Invalid length.") | ||
| if ( | ||
| TABLE[v[0]] == 0xFF | ||
| and TABLE[v[1]] == 0xFF | ||
| and TABLE[v[2]] == 0xFF | ||
| and TABLE[v[3]] == 0xFF | ||
| and TABLE[v[4]] == 0xFF | ||
| and TABLE[v[5]] == 0xFF | ||
| and TABLE[v[6]] == 0xFF | ||
| and TABLE[v[7]] == 0xFF | ||
| and TABLE[v[8]] == 0xFF | ||
| and TABLE[v[9]] == 0xFF | ||
| and TABLE[v[10]] == 0xFF | ||
| and TABLE[v[11]] == 0xFF | ||
| and TABLE[v[12]] == 0xFF | ||
| and TABLE[v[13]] == 0xFF | ||
| and TABLE[v[14]] == 0xFF | ||
| and TABLE[v[15]] == 0xFF | ||
| and TABLE[v[16]] == 0xFF | ||
| and TABLE[v[17]] == 0xFF | ||
| and TABLE[v[18]] == 0xFF | ||
| and TABLE[v[19]] == 0xFF | ||
| and TABLE[v[20]] == 0xFF | ||
| and TABLE[v[21]] == 0xFF | ||
| and TABLE[v[22]] == 0xFF | ||
| and TABLE[v[23]] == 0xFF | ||
| and TABLE[v[24]] == 0xFF | ||
| and TABLE[v[25]] == 0xFF | ||
| ): | ||
| raise RuntimeError("Invalid base32 character") | ||
| v = s.encode("utf-8") | ||
| tbl = TABLE | ||
| typeid = [0] * 16 | ||
| for b in v: | ||
| if tbl[b] == 0xFF: | ||
| raise RuntimeError("Invalid base32 character") | ||
| out = bytearray(16) | ||
| # 6 bytes timestamp (48 bits) | ||
| typeid[0] = (TABLE[v[0]] << 5) | TABLE[v[1]] | ||
| typeid[1] = (TABLE[v[2]] << 3) | (TABLE[v[3]] >> 2) | ||
| typeid[2] = ((TABLE[v[3]] & 3) << 6) | (TABLE[v[4]] << 1) | (TABLE[v[5]] >> 4) | ||
| typeid[3] = ((TABLE[v[5]] & 15) << 4) | (TABLE[v[6]] >> 1) | ||
| typeid[4] = ((TABLE[v[6]] & 1) << 7) | (TABLE[v[7]] << 2) | (TABLE[v[8]] >> 3) | ||
| typeid[5] = ((TABLE[v[8]] & 7) << 5) | TABLE[v[9]] | ||
| out[0] = (tbl[v[0]] << 5) | tbl[v[1]] | ||
| out[1] = (tbl[v[2]] << 3) | (tbl[v[3]] >> 2) | ||
| out[2] = ((tbl[v[3]] & 3) << 6) | (tbl[v[4]] << 1) | (tbl[v[5]] >> 4) | ||
| out[3] = ((tbl[v[5]] & 15) << 4) | (tbl[v[6]] >> 1) | ||
| out[4] = ((tbl[v[6]] & 1) << 7) | (tbl[v[7]] << 2) | (tbl[v[8]] >> 3) | ||
| out[5] = ((tbl[v[8]] & 7) << 5) | tbl[v[9]] | ||
| # 10 bytes of entropy (80 bits) | ||
| typeid[6] = (TABLE[v[10]] << 3) | (TABLE[v[11]] >> 2) | ||
| typeid[7] = ((TABLE[v[11]] & 3) << 6) | (TABLE[v[12]] << 1) | (TABLE[v[13]] >> 4) | ||
| typeid[8] = ((TABLE[v[13]] & 15) << 4) | (TABLE[v[14]] >> 1) | ||
| typeid[9] = ((TABLE[v[14]] & 1) << 7) | (TABLE[v[15]] << 2) | (TABLE[v[16]] >> 3) | ||
| typeid[10] = ((TABLE[v[16]] & 7) << 5) | TABLE[v[17]] | ||
| typeid[11] = (TABLE[v[18]] << 3) | (TABLE[v[19]] >> 2) | ||
| typeid[12] = ((TABLE[v[19]] & 3) << 6) | (TABLE[v[20]] << 1) | (TABLE[v[21]] >> 4) | ||
| typeid[13] = ((TABLE[v[21]] & 15) << 4) | (TABLE[v[22]] >> 1) | ||
| typeid[14] = ((TABLE[v[22]] & 1) << 7) | (TABLE[v[23]] << 2) | (TABLE[v[24]] >> 3) | ||
| typeid[15] = ((TABLE[v[24]] & 7) << 5) | TABLE[v[25]] | ||
| # 10 bytes entropy (80 bits) | ||
| out[6] = (tbl[v[10]] << 3) | (tbl[v[11]] >> 2) | ||
| out[7] = ((tbl[v[11]] & 3) << 6) | (tbl[v[12]] << 1) | (tbl[v[13]] >> 4) | ||
| out[8] = ((tbl[v[13]] & 15) << 4) | (tbl[v[14]] >> 1) | ||
| out[9] = ((tbl[v[14]] & 1) << 7) | (tbl[v[15]] << 2) | (tbl[v[16]] >> 3) | ||
| out[10] = ((tbl[v[16]] & 7) << 5) | tbl[v[17]] | ||
| out[11] = (tbl[v[18]] << 3) | (tbl[v[19]] >> 2) | ||
| out[12] = ((tbl[v[19]] & 3) << 6) | (tbl[v[20]] << 1) | (tbl[v[21]] >> 4) | ||
| out[13] = ((tbl[v[21]] & 15) << 4) | (tbl[v[22]] >> 1) | ||
| out[14] = ((tbl[v[22]] & 1) << 7) | (tbl[v[23]] << 2) | (tbl[v[24]] >> 3) | ||
| out[15] = ((tbl[v[24]] & 7) << 5) | tbl[v[25]] | ||
| return typeid | ||
| return bytes(out) | ||
| def encode(src: bytes) -> str: | ||
| if _HAS_RUST: | ||
| return _encode_rust(src) | ||
| return _encode_py(src) | ||
| def decode(s: str) -> bytes: | ||
| if _HAS_RUST: | ||
| return _decode_rust(s) | ||
| return _decode_py(s) |
@@ -134,5 +134,7 @@ """ | ||
| created_at = _uuid7_created_at(uuid_obj) | ||
| sortable = True # UUIDv7 is time-ordered by design | ||
| ver = _uuid_version(uuid_obj) | ||
| created_at = _uuid7_created_at(uuid_obj) if ver == 7 else None | ||
| sortable = True if ver == 7 else False | ||
| return ParsedTypeID( | ||
@@ -249,1 +251,9 @@ raw=id_str, | ||
| exp.provenance.setdefault("sortable", Provenance.DERIVED_FROM_ID) | ||
| def _uuid_version(u: Any) -> Optional[int]: | ||
| try: | ||
| # uuid.UUID and uuid6.UUID both usually expose .version | ||
| return int(u.version) | ||
| except Exception: | ||
| return None |
+120
-37
@@ -1,14 +0,31 @@ | ||
| import uuid | ||
| from datetime import datetime, timezone | ||
| import warnings | ||
| from typing import Generic, Optional, TypeVar | ||
| import uuid6 | ||
| import uuid as std_uuid | ||
| from typeid import base32 | ||
| from typeid.errors import InvalidTypeIDStringException | ||
| from typeid.validation import validate_prefix, validate_suffix | ||
| from typeid.validation import validate_prefix, validate_suffix_and_decode | ||
| from typeid._uuid_backend import get_uuid_backend | ||
| _backend = get_uuid_backend() | ||
| PrefixT = TypeVar("PrefixT", bound=str) | ||
| def _uuid_from_bytes_v7(uuid_bytes: bytes) -> std_uuid.UUID: | ||
| """ | ||
| Construct a UUID object from bytes. | ||
| Prefer uuid6 (if installed) to preserve UUIDv7 semantics like `.time`. | ||
| """ | ||
| try: | ||
| import uuid6 # type: ignore | ||
| uuid_int = int.from_bytes(uuid_bytes, "big") | ||
| return uuid6.UUID(int=uuid_int) | ||
| except Exception: | ||
| return std_uuid.UUID(bytes=uuid_bytes) | ||
| class TypeID(Generic[PrefixT]): | ||
@@ -37,2 +54,4 @@ """ | ||
| __slots__ = ("_prefix", "_suffix", "_uuid_bytes", "_uuid", "_str") | ||
| def __init__(self, prefix: Optional[PrefixT] = None, suffix: Optional[str] = None) -> None: | ||
@@ -54,15 +73,25 @@ """ | ||
| """ | ||
| # If no suffix is provided, generate a new UUIDv7 and encode it as Base32. | ||
| suffix = _convert_uuid_to_b32(uuid6.uuid7()) if not suffix else suffix | ||
| # Validate prefix early (cheap) so failures don't do extra work | ||
| if prefix: | ||
| validate_prefix(prefix=prefix) | ||
| self._prefix: Optional[PrefixT] = prefix | ||
| # Ensure the suffix is a valid encoded UUID representation. | ||
| validate_suffix(suffix=suffix) | ||
| self._str: Optional[str] = None | ||
| self._uuid: Optional[std_uuid.UUID] = None | ||
| self._uuid_bytes: Optional[bytes] = None | ||
| # Prefix is optional; when present it must satisfy the project's prefix rules. | ||
| if prefix is not None: | ||
| validate_prefix(prefix=prefix) | ||
| if not suffix: | ||
| # generate uuid (fast path) | ||
| u = _backend.uuid7() | ||
| uuid_bytes = u.bytes | ||
| suffix = base32.encode(uuid_bytes) | ||
| # Cache UUID object (keep original type for user expectations) | ||
| self._uuid = u | ||
| self._uuid_bytes = uuid_bytes | ||
| else: | ||
| # validate+decode once; don't create UUID object yet | ||
| uuid_bytes = validate_suffix_and_decode(suffix) | ||
| self._uuid_bytes = uuid_bytes | ||
| # Keep prefix as Optional internally. String rendering decides whether to show it. | ||
| self._prefix: Optional[PrefixT] = prefix | ||
| self._suffix: str = suffix | ||
| self._suffix = suffix | ||
@@ -93,3 +122,3 @@ @classmethod | ||
| @classmethod | ||
| def from_uuid(cls, suffix: uuid.UUID, prefix: Optional[PrefixT] = None) -> "TypeID": | ||
| def from_uuid(cls, suffix: std_uuid.UUID, prefix: Optional[PrefixT] = None) -> "TypeID": | ||
| """ | ||
@@ -108,6 +137,17 @@ Construct a TypeID from an existing UUID. | ||
| """ | ||
| # Encode the UUID into the canonical Base32 suffix representation. | ||
| suffix_str = _convert_uuid_to_b32(suffix) | ||
| return cls(suffix=suffix_str, prefix=prefix) | ||
| # Validate prefix (if provided) | ||
| if prefix: | ||
| validate_prefix(prefix=prefix) | ||
| uuid_bytes = suffix.bytes | ||
| suffix_str = base32.encode(uuid_bytes) | ||
| obj = cls.__new__(cls) # bypass __init__ to avoid decode+validate cycle | ||
| obj._prefix = prefix | ||
| obj._suffix = suffix_str | ||
| obj._uuid_bytes = uuid_bytes | ||
| obj._uuid = suffix # keep original object type (uuid6/uuid_utils/stdlib) | ||
| obj._str = None | ||
| return obj | ||
| @property | ||
@@ -139,3 +179,3 @@ def suffix(self) -> str: | ||
| @property | ||
| def uuid(self) -> uuid6.UUID: | ||
| def uuid(self) -> std_uuid.UUID: | ||
| """ | ||
@@ -146,9 +186,57 @@ The UUID represented by this TypeID. | ||
| The decoded UUID value. | ||
| """ | ||
| # Lazy materialization | ||
| if self._uuid is None: | ||
| assert self._uuid_bytes is not None | ||
| self._uuid = _uuid_from_bytes_v7(self._uuid_bytes) | ||
| return self._uuid | ||
| Notes: | ||
| - This decodes `self.suffix` each time it is accessed. | ||
| - The UUID type here follows `uuid6.UUID` used by the project. | ||
| @property | ||
| def uuid_bytes(self) -> bytes: | ||
| """ | ||
| return _convert_b32_to_uuid(self.suffix) | ||
| Raw bytes of the underlying UUID. | ||
| This returns the canonical 16-byte representation of the UUID encoded | ||
| in this TypeID. The value is derived lazily from the suffix and cached | ||
| on first access. | ||
| This property is backend-agnostic and independent of the concrete | ||
| UUID implementation used internally. | ||
| Returns: | ||
| A 16-byte ``bytes`` object representing the UUID. | ||
| """ | ||
| if self._uuid_bytes is None: | ||
| self._uuid_bytes = base32.decode(self._suffix) | ||
| return self._uuid_bytes | ||
| @property | ||
| def created_at(self) -> Optional[datetime]: | ||
| """ | ||
| Creation time embedded in the underlying UUID, if available. | ||
| TypeID typically uses UUIDv7 for generated IDs. UUIDv7 encodes the Unix | ||
| timestamp (milliseconds) in the most significant 48 bits of the 128-bit UUID. | ||
| Returns: | ||
| A timezone-aware UTC datetime if the underlying UUID is version 7, | ||
| otherwise None. | ||
| """ | ||
| u = self.uuid | ||
| # Only UUIDv7 has a defined "created_at" in this sense. | ||
| try: | ||
| if getattr(u, "version", None) != 7: | ||
| return None | ||
| except Exception: | ||
| return None | ||
| try: | ||
| # UUID is 128 bits; top 48 bits are unix epoch time in milliseconds. | ||
| # So: unix_ms = uuid_int >> (128 - 48) = uuid_int >> 80 | ||
| unix_ms = int(u.int) >> 80 | ||
| return datetime.fromtimestamp(unix_ms / 1000.0, tz=timezone.utc) | ||
| except Exception: | ||
| return None | ||
| def __str__(self) -> str: | ||
@@ -161,7 +249,12 @@ """ | ||
| """ | ||
| value = "" | ||
| # cache string representation; helps workflow + comparisons | ||
| s = self._str | ||
| if s is not None: | ||
| return s | ||
| if self.prefix: | ||
| value += f"{self.prefix}_" | ||
| value += self.suffix | ||
| return value | ||
| s = f"{self.prefix}_{self.suffix}" | ||
| else: | ||
| s = self.suffix | ||
| self._str = s | ||
| return s | ||
@@ -224,3 +317,3 @@ def __repr__(self): | ||
| def from_uuid(suffix: uuid.UUID, prefix: Optional[str] = None) -> TypeID: | ||
| def from_uuid(suffix: std_uuid.UUID, prefix: Optional[str] = None) -> TypeID: | ||
| warnings.warn("Consider TypeID.from_uuid instead.", DeprecationWarning) | ||
@@ -245,11 +338,1 @@ return TypeID.from_uuid(suffix=suffix, prefix=prefix) | ||
| return prefix, suffix | ||
| def _convert_uuid_to_b32(uuid_instance: uuid.UUID) -> str: | ||
| return base32.encode(list(uuid_instance.bytes)) | ||
| def _convert_b32_to_uuid(b32: str) -> uuid6.UUID: | ||
| uuid_bytes = bytes(base32.decode(b32)) | ||
| uuid_int = int.from_bytes(uuid_bytes, byteorder="big") | ||
| return uuid6.UUID(int=uuid_int, version=7) |
+15
-4
@@ -7,10 +7,16 @@ import re | ||
| _PREFIX_RE = re.compile(r"^([a-z]([a-z0-9_]{0,61}[a-z0-9])?)?$") # allow digits too (spec-like) | ||
| def validate_prefix(prefix: str) -> None: | ||
| # See https://github.com/jetify-com/typeid/tree/main/spec | ||
| if not re.match("^([a-z]([a-z_]{0,61}[a-z])?)?$", prefix): | ||
| # Use fullmatch (anchored) and precompiled regex | ||
| if not _PREFIX_RE.fullmatch(prefix or ""): | ||
| raise PrefixValidationException(f"Invalid prefix: {prefix}.") | ||
| def validate_suffix(suffix: str) -> None: | ||
| def validate_suffix_and_decode(suffix: str) -> bytes: | ||
| """ | ||
| Validate a TypeID suffix and return decoded UUID bytes (16 bytes). | ||
| This guarantees: one decode per suffix on the fast path. | ||
| """ | ||
| if ( | ||
@@ -25,5 +31,10 @@ len(suffix) != SUFFIX_LEN | ||
| raise SuffixValidationException(f"Invalid suffix: {suffix}.") | ||
| try: | ||
| base32.decode(suffix) | ||
| uuid_bytes = base32.decode(suffix) # rust-backed or py fallback | ||
| except Exception as exc: | ||
| raise SuffixValidationException(f"Invalid suffix: {suffix}.") from exc | ||
| if len(uuid_bytes) != 16: | ||
| raise SuffixValidationException(f"Invalid suffix: {suffix}.") | ||
| return uuid_bytes |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
21
5%1593
0.06%78167
-0.08%