New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

typeid-python

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

typeid-python - pypi Package Compare versions

Comparing version
0.3.7
to
0.3.8
+5
typeid/cli/__init__.py
from typeid.cli.main import cli
if __name__ == "__main__":
cli()
from pathlib import Path
from typing import Optional
import click
from uuid_utils import UUID
from typeid import TypeID
from typeid.codecs import base32
from typeid.core.parsing import get_prefix_and_suffix
from typeid.explain.discovery import discover_schema_path
from typeid.explain.engine import explain as explain_engine
from typeid.explain.formatters import format_explanation_json, format_explanation_pretty
from typeid.explain.registry import load_registry, make_lookup
@click.group()
def cli():
# Root CLI command group.
# This acts as the entry point for all subcommands.
pass
@cli.command()
@click.option("-p", "--prefix")
def new(prefix: Optional[str] = None) -> None:
"""
Generate a new TypeID.
If a prefix is provided, it will be validated and included in the output.
If no prefix is provided, a prefix-less TypeID is generated.
"""
typeid = TypeID(prefix=prefix)
click.echo(str(typeid))
@cli.command()
@click.argument("uuid")
@click.option("-p", "--prefix")
def encode(uuid: str, prefix: Optional[str] = None) -> None:
"""
Encode an existing UUID into a TypeID.
This command is intended for cases where UUIDs already exist
(e.g. stored in a database) and need to be represented as TypeIDs.
"""
uuid_obj = UUID(uuid)
typeid = TypeID.from_uuid(suffix=uuid_obj, prefix=prefix)
click.echo(str(typeid))
@cli.command()
@click.argument("encoded")
def decode(encoded: str) -> None:
"""
Decode a TypeID into its components.
This extracts:
- the prefix (if any)
- the underlying UUID
This command is primarily intended for inspection and debugging.
"""
prefix, suffix = get_prefix_and_suffix(encoded)
decoded_bytes = bytes(base32.decode(suffix))
uuid = UUID(bytes=decoded_bytes)
click.echo(f"type: {prefix}")
click.echo(f"uuid: {uuid}")
@cli.command()
@click.argument("encoded")
@click.option(
"--schema",
"schema_path",
type=click.Path(exists=True, dir_okay=False, path_type=str),
required=False,
help="Path to TypeID schema file (JSON, or YAML if PyYAML is installed). "
"If omitted, TypeID will try to discover a schema automatically.",
)
@click.option(
"--json",
"as_json",
is_flag=True,
help="Output machine-readable JSON.",
)
@click.option(
"--no-schema",
is_flag=True,
help="Disable schema lookup (derived facts only).",
)
@click.option(
"--no-links",
is_flag=True,
help="Disable link template rendering.",
)
def explain(
encoded: str,
schema_path: Optional[str],
as_json: bool,
no_schema: bool,
no_links: bool,
) -> None:
"""
Explain a TypeID: parse/validate it, derive facts (uuid, created_at),
and optionally enrich explanation from a user-provided schema.
"""
enable_schema = not no_schema
enable_links = not no_links
schema_lookup = None
warnings: list[str] = []
# Load schema (optional)
if enable_schema:
resolved_path = None
if schema_path:
resolved_path = schema_path
else:
discovery = discover_schema_path()
if discovery.path is not None:
resolved_path = str(discovery.path)
# If env var was set but invalid, discovery returns source info;
# we keep CLI robust and simply proceed without schema.
if resolved_path:
result = load_registry(Path(resolved_path))
if result.registry is not None:
schema_lookup = make_lookup(result.registry)
else:
if result.error is not None:
warnings.append(f"Schema load failed: {result.error.message}")
# Build explanation (never raises on normal errors)
exp = explain_engine(
encoded,
schema_lookup=schema_lookup,
enable_schema=enable_schema,
enable_links=enable_links,
)
# Surface schema-load warnings (if any)
if warnings:
exp.warnings.extend(warnings)
# Print
if as_json:
click.echo(format_explanation_json(exp))
else:
click.echo(format_explanation_pretty(exp))
from typeid._base32 import encode as _encode_rust, decode as _decode_rust # type: ignore
def encode(src: bytes) -> str:
"""
Encode 16 raw bytes into a 26-character TypeID suffix (Rust-accelerated).
This function is the low-level codec used by TypeID to transform the 16-byte
UUID payload into the 26-character suffix string.
It is **not** a general-purpose Base32 encoder:
- Input length is strictly **16 bytes**.
- Output length is strictly **26 characters**.
- The alphabet is fixed to:
``0123456789abcdefghjkmnpqrstvwxyz`` (lowercase only).
The mapping is a fixed bit-packing scheme:
- The first 6 input bytes (48 bits) become the first 10 characters
(often corresponding to the UUIDv7 timestamp portion in TypeID usage).
- The remaining 10 bytes (80 bits) become the last 16 characters.
Parameters
----------
src : bytes
Exactly 16 bytes to encode (the raw UUID bytes).
Returns
-------
str
A 26-character ASCII string using only the TypeID Base32 alphabet.
Raises
------
RuntimeError
If ``src`` is not exactly 16 bytes long:
``"Invalid length (expected 16 bytes)."``
Notes
-----
- This function is implemented in Rust and exposed via a CPython extension;
there is no Python fallback.
- The returned string is always lowercase.
"""
return _encode_rust(src)
def decode(s: str) -> bytes:
"""
Decode a 26-character TypeID suffix into 16 raw bytes (Rust-accelerated).
This function is the inverse of :func:`encode`. It takes a TypeID suffix
(26 characters) and reconstructs the original 16 bytes by reversing the
same fixed bit-packing scheme.
Decoding is strict:
- Input length must be exactly **26 characters**.
- Only characters from the alphabet
``0123456789abcdefghjkmnpqrstvwxyz`` are accepted.
- Uppercase letters, whitespace, separators, and Crockford aliases
(e.g. 'O'→'0', 'I'/'L'→'1') are **not** accepted.
Parameters
----------
s : str
The 26-character suffix string to decode.
Returns
-------
bytes
Exactly 16 bytes (the raw UUID bytes).
Raises
------
RuntimeError
If ``s`` is not exactly 26 characters long:
``"Invalid length (expected 26 chars)."``
If ``s`` contains any character outside the allowed alphabet:
``"Invalid base32 character."``
Notes
-----
- This function is implemented in Rust and exposed via a CPython extension;
there is no Python fallback.
- This performs only decoding/validation of the suffix encoding. It does not
validate UUID version/variant semantics.
"""
return _decode_rust(s)
SUFFIX_LEN = 26
PREFIX_MAX_LEN = 63
ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz"
class TypeIDException(Exception):
...
class PrefixValidationException(TypeIDException):
...
class SuffixValidationException(TypeIDException):
...
class InvalidTypeIDStringException(TypeIDException):
...
from dataclasses import dataclass
from functools import lru_cache
from typing import Callable
from typeid.core.typeid import TypeID
@dataclass(frozen=True, slots=True)
class TypeIDFactory:
"""
Callable object that generates TypeIDs with a fixed prefix.
Example:
user_id = TypeIDFactory("user")()
"""
prefix: str
def __call__(self) -> TypeID:
return TypeID(self.prefix)
def typeid_factory(prefix: str) -> Callable[[], TypeID]:
"""
Return a zero-argument callable that generates TypeIDs with a fixed prefix.
Example:
user_id = typeid_factory("user")()
"""
return TypeIDFactory(prefix)
@lru_cache(maxsize=256)
def cached_typeid_factory(prefix: str) -> Callable[[], TypeID]:
"""
Same as typeid_factory, but caches factories by prefix.
Use this if you create factories repeatedly at runtime.
"""
return TypeIDFactory(prefix)
from typeid.core.errors import InvalidTypeIDStringException
def get_prefix_and_suffix(string: str) -> tuple:
parts = string.rsplit("_", 1)
# When there's no underscore in the string.
if len(parts) == 1:
if parts[0].strip() == "":
raise InvalidTypeIDStringException(f"Invalid TypeID: {string}")
return None, parts[0]
# When there is an underscore, unpack prefix and suffix.
prefix, suffix = parts
if prefix.strip() == "" or suffix.strip() == "":
raise InvalidTypeIDStringException(f"Invalid TypeID: {string}")
return prefix, suffix
from datetime import datetime, timezone
import warnings
import uuid_utils
from typing import Generic, Optional, TypeVar
from typeid.codecs import base32
from typeid.core.parsing import get_prefix_and_suffix
from typeid.core.validation import validate_prefix, validate_suffix_and_decode
PrefixT = TypeVar("PrefixT", bound=str)
def _uuid_from_bytes_v7(uuid_bytes: bytes) -> uuid_utils.UUID:
"""
Construct a UUID object from bytes.
"""
uuid_int = int.from_bytes(uuid_bytes, "big")
return uuid_utils.UUID(int=uuid_int)
class TypeID(Generic[PrefixT]):
"""
A TypeID is a human-meaningful, UUID-backed identifier.
A TypeID is rendered as:
<prefix>_<suffix> or just <suffix> (when prefix is None/empty)
- **prefix**: optional semantic label (e.g. "user", "order"). It is *not* part of the UUID.
Prefixes are validated for allowed characters/shape (see `validate_prefix`).
- **suffix**: a compact, URL-safe Base32 encoding of a UUID (UUIDv7 by default).
Suffixes are validated structurally (see `validate_suffix`).
Design notes:
- A TypeID is intended to be safe to store as a string (e.g. in logs / URLs).
- The underlying UUID can always be recovered via `.uuid`.
- Ordering (`>`, `>=`) is based on lexicographic order of the string representation,
which corresponds to time-ordering if the UUID version is time-sortable (UUIDv7).
Type parameters:
PrefixT: a type-level constraint for the prefix (often `str` or a Literal).
"""
__slots__ = ("_prefix", "_suffix", "_uuid_bytes", "_uuid", "_str")
def __init__(self, prefix: Optional[PrefixT] = None, suffix: Optional[str] = None) -> None:
"""
Create a new TypeID.
If `suffix` is not provided, a new UUIDv7 is generated and encoded as Base32.
If `prefix` is provided, it is validated.
Args:
prefix: Optional prefix. If None, the TypeID has no prefix and its string
form will be just the suffix. If provided, it must pass `validate_prefix`.
suffix: Optional Base32-encoded UUID string. If None, a new UUIDv7 is generated.
Raises:
InvalidTypeIDStringException (or another project-specific exception):
If `suffix` is invalid, or if `prefix` is invalid.
"""
# Validate prefix early (cheap) so failures don't do extra work
if prefix:
validate_prefix(prefix=prefix)
self._prefix: Optional[PrefixT] = prefix
self._str: Optional[str] = None
self._uuid: Optional[uuid_utils.UUID] = None
self._uuid_bytes: Optional[bytes] = None
if not suffix:
# generate uuid (fast path)
u = uuid_utils.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
self._suffix = suffix
@classmethod
def from_string(cls, string: str) -> "TypeID":
"""
Parse a TypeID from its string form.
The input can be either:
- "<prefix>_<suffix>"
- "<suffix>" (prefix-less)
Args:
string: String representation of a TypeID.
Returns:
A `TypeID` instance.
Raises:
InvalidTypeIDStringException (or another project-specific exception):
If the string cannot be split/parsed or if the extracted parts are invalid.
"""
# Split into (prefix, suffix) according to project rules.
prefix, suffix = get_prefix_and_suffix(string=string)
return cls(suffix=suffix, prefix=prefix)
@classmethod
def from_uuid(cls, suffix: uuid_utils.UUID, prefix: Optional[PrefixT] = None) -> "TypeID":
"""
Construct a TypeID from an existing UUID.
This is useful when you store UUIDs in a database but want to expose
TypeIDs at the application boundary.
Args:
suffix: UUID value to encode into the TypeID suffix.
prefix: Optional prefix to attach (validated if provided).
Returns:
A `TypeID` whose `.uuid` equals the provided UUID.
"""
# 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
obj._str = None
return obj
@property
def suffix(self) -> str:
"""
The Base32-encoded UUID portion of the TypeID (always present).
Notes:
- This is the identity-carrying part.
- It is validated at construction time.
"""
return self._suffix
@property
def prefix(self) -> str:
"""
The prefix portion of the TypeID, as a string.
Returns:
The configured prefix, or "" if the TypeID is prefix-less.
Notes:
- Empty string is the *presentation* of "no prefix". Internally, `_prefix`
remains Optional to preserve the distinction between None and a real value.
"""
return self._prefix or ""
@property
def uuid(self) -> uuid_utils.UUID:
"""
The UUID represented by this TypeID.
Returns:
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
@property
def uuid_bytes(self) -> bytes:
"""
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.
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:
"""
Render the TypeID into its canonical string representation.
Returns:
"<prefix>_<suffix>" if prefix is present, otherwise "<suffix>".
"""
# cache string representation; helps workflow + comparisons
s = self._str
if s is not None:
return s
if self.prefix:
s = f"{self.prefix}_{self.suffix}"
else:
s = self.suffix
self._str = s
return s
def __repr__(self):
"""
Developer-friendly representation.
Uses a constructor-like form to make debugging and copy/paste easier.
"""
return "%s.from_string(%r)" % (self.__class__.__name__, str(self))
def __eq__(self, value: object) -> bool:
"""
Equality based on prefix and suffix.
Notes:
- Two TypeIDs are considered equal if both their string components match.
- This is stricter than "same UUID" because prefix is part of the public ID.
"""
if not isinstance(value, TypeID):
return False
return value.prefix == self.prefix and value.suffix == self.suffix
def __gt__(self, other) -> bool:
"""
Compare TypeIDs by lexicographic order of their string form.
This is useful because TypeID suffixes based on UUIDv7 are time-sortable,
so string order typically corresponds to creation time order (within a prefix).
Returns:
True/False if `other` is a TypeID, otherwise NotImplemented.
"""
if isinstance(other, TypeID):
return str(self) > str(other)
return NotImplemented
def __ge__(self, other) -> bool:
"""
Compare TypeIDs by lexicographic order of their string form (>=).
See `__gt__` for rationale and notes.
"""
if isinstance(other, TypeID):
return str(self) >= str(other)
return NotImplemented
def __hash__(self) -> int:
"""
Hash based on (prefix, suffix), allowing TypeIDs to be used as dict keys / set members.
"""
return hash((self.prefix, self.suffix))
def from_string(string: str) -> TypeID:
warnings.warn("Consider TypeID.from_string instead.", DeprecationWarning)
return TypeID.from_string(string=string)
def from_uuid(suffix: uuid_utils.UUID, prefix: Optional[str] = None) -> TypeID:
warnings.warn("Consider TypeID.from_uuid instead.", DeprecationWarning)
return TypeID.from_uuid(suffix=suffix, prefix=prefix)
import re
from typeid.codecs import base32
from typeid.core.constants import SUFFIX_LEN, ALPHABET
from typeid.core.errors import PrefixValidationException, SuffixValidationException
_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:
# Use fullmatch (anchored) and precompiled regex
if not _PREFIX_RE.fullmatch(prefix or ""):
raise PrefixValidationException(f"Invalid prefix: {prefix}.")
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 (
len(suffix) != SUFFIX_LEN
or suffix == ""
or " " in suffix
or (not suffix.isdigit() and not suffix.islower())
or any([symbol not in ALPHABET for symbol in suffix])
or suffix[0] > "7"
):
raise SuffixValidationException(f"Invalid suffix: {suffix}.")
try:
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
from .v2 import TypeIDField
__all__ = ["TypeIDField"]
from dataclasses import dataclass
from typing import Any, ClassVar, Generic, Literal, Optional, TypeVar, get_args, get_origin
from pydantic_core import core_schema
from pydantic.json_schema import JsonSchemaValue
from typeid import TypeID
T = TypeVar("T")
def _parse_typeid(value: Any) -> TypeID:
"""
Convert input into a TypeID instance.
Supports:
- TypeID -> TypeID
- str -> parse into TypeID
"""
if isinstance(value, TypeID):
return value
if isinstance(value, str):
return TypeID.from_string(value)
raise TypeError(f"TypeID must be str or TypeID, got {type(value).__name__}")
@dataclass(frozen=True)
class _TypeIDMeta:
expected_prefix: Optional[str] = None
pattern: Optional[str] = None
example: Optional[str] = None
class _TypeIDFieldBase:
"""
Base class implementing Pydantic v2 hooks.
Subclasses specify _typeid_meta.
"""
_typeid_meta: ClassVar[_TypeIDMeta] = _TypeIDMeta()
@classmethod
def _validate(cls, v: Any) -> TypeID:
tid = _parse_typeid(v)
exp = cls._typeid_meta.expected_prefix
if exp is not None:
if tid.prefix != exp:
raise ValueError(f"TypeID prefix mismatch: expected '{exp}', got '{tid.prefix}'")
return tid
@classmethod
def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> core_schema.CoreSchema:
"""
Build a schema that:
- accepts TypeID instances
- accepts strings and validates/parses them
- serializes to string in JSON
"""
# Accept either already-parsed TypeID, or a string (or any -> we validate)
# Using a plain validator keeps it simple and fast.
return core_schema.no_info_plain_validator_function(
cls._validate,
serialization=core_schema.plain_serializer_function_ser_schema(
lambda v: str(v),
when_used="json",
),
)
@classmethod
def __get_pydantic_json_schema__(cls, core_schema_: core_schema.CoreSchema, handler: Any) -> JsonSchemaValue:
schema = handler(core_schema_)
# Ensure JSON schema is "string"
schema.update(
{
"type": "string",
"format": "typeid",
}
)
# Add prefix hint in schema
exp = cls._typeid_meta.expected_prefix
if exp is not None:
schema.setdefault("description", f"TypeID with prefix '{exp}'")
# Optional pattern / example
if cls._typeid_meta.pattern:
schema["pattern"] = cls._typeid_meta.pattern
if cls._typeid_meta.example:
schema.setdefault("examples", [cls._typeid_meta.example])
return schema
class TypeIDField(Generic[T]):
"""
Usage:
from typeid.integrations.pydantic import TypeIDField
class User(BaseModel):
id: TypeIDField["user"]
This returns a specialized *type* that Pydantic will validate into your core TypeID.
"""
def __class_getitem__(cls, item: str | tuple[str]) -> type[TypeID]:
"""
Support:
- TypeIDField["user"]
- TypeIDField[Literal["user"]]
- TypeIDField[("user",)]
"""
if isinstance(item, tuple):
if len(item) != 1:
raise TypeError("TypeIDField[...] expects a single prefix")
item = item[0]
# Literal["user"]
if get_origin(item) is Literal:
args = get_args(item)
if len(args) != 1 or not isinstance(args[0], str):
raise TypeError("TypeIDField[Literal['prefix']] expects a single string literal")
prefix = args[0]
# Plain "user"
elif isinstance(item, str):
prefix = item
else:
raise TypeError("TypeIDField[...] expects a string prefix or Literal['prefix']")
name = f"TypeIDField_{prefix}"
# Optionally add a simple example that looks like TypeID format
# You can improve this to a real example generator if your core has one.
example = f"{prefix}_01hxxxxxxxxxxxxxxxxxxxxxxx"
# Create a new subclass of _TypeIDFieldBase with fixed meta
field_cls = type(
name,
(_TypeIDFieldBase,),
{
"_typeid_meta": _TypeIDMeta(
expected_prefix=prefix,
# If you know your precise regex, put it here:
# pattern=rf"^{prefix}_[0-9a-z]{{26}}$",
pattern=None,
example=example,
)
},
)
# IMPORTANT:
# We return `field_cls` as the annotation type, but the runtime validated value is your core TypeID.
return field_cls # type: ignore[return-value]
+38
-11
Metadata-Version: 2.4
Name: typeid-python
Version: 0.3.7
Version: 0.3.8
Classifier: Development Status :: 3 - Alpha

@@ -23,13 +23,15 @@ Classifier: License :: OSI Approved :: MIT License

Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Bug Tracker, https://github.com/akhundMurad/typeid-python/issues
Project-URL: Homepage, https://github.com/akhundMurad/typeid-python
Project-URL: Repository, https://github.com/akhundMurad/typeid-python
Project-URL: Bug Tracker, https://github.com/akhundMurad/typeid-python/issues
# TypeID Python
[![Run Tests](https://github.com/akhundMurad/typeid-python/actions/workflows/test.yml/badge.svg)](https://github.com/akhundMurad/typeid-python/actions/workflows/test.yml)
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/typeid-python?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/typeid-python)
[![PyPI - Version](https://img.shields.io/pypi/v/typeid-python?color=green)](https://pypi.org/project/typeid-python/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/typeid-python?color=green)](https://pypi.org/project/typeid-python/)
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/typeid-python?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/typeid-python)
![GitHub License](https://img.shields.io/github/license/akhundMurad/typeid-python)
> [!WARNING]
> `main` may contain unreleased changes. For stable usage, use the latest release tag.

@@ -46,3 +48,3 @@ A **high-performance Python implementation of [TypeIDs](https://github.com/jetpack-io/typeid)** — type-safe,

This library provides a Python package with optional Rust acceleration.
This library provides a Python package with Rust acceleration.

@@ -52,7 +54,8 @@ ## Key features

- ✅ UUIDv7-based, time-sortable identifiers
- ✅ Type-safe prefixes (`user_`, `order_`, …)
- ✅ Schema-based ID explanations (JSON / YAML)
- ✅ Fast generation & parsing (Rust-accelerated)
- ✅ Multiple integrations (Pydantic, FastAPI, ...)
- ✅ 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

@@ -102,4 +105,4 @@

```console
$ pip install typeid-python[yaml] # YAML schema support
$ pip install typeid-python[cli] # CLI tools
$ pip install typeid-python[yaml] # YAML schema support
$ pip install typeid-python[cli] # CLI tools
```

@@ -182,2 +185,27 @@

## Framework integrations
TypeID is **framework-agnostic by design**.
Integrations are provided as optional adapters, installed explicitly and kept separate from the core.
### Available integrations
* **Pydantic (v2)**
Native field type with validation and JSON Schema support.
```python
from typing import Literal
from pydantic import BaseModel
from typeid.integrations.pydantic import TypeIDField
class User(BaseModel):
id: TypeIDField[Literal["user"]]
```
* **FastAPI** (Coming Soon 🚧)
* **SQLAlchemy** (Coming Soon 🚧)
All integrations are **opt-in via extras** and never affect the core package.
## ✨ `typeid explain` — understand any ID

@@ -231,3 +259,2 @@

* **Non-breaking**: stable APIs
* **Optional acceleration**: Rust is opt-in
* **Lazy evaluation**: work is done only when needed

@@ -234,0 +261,0 @@ * **Explainability**: identifiers carry meaning

[project]
name = "typeid-python"
version = "0.3.7"
version = "0.3.8"
description = "Python implementation of TypeIDs: type-safe, K-sortable, and globally unique identifiers inspired by Stripe IDs"

@@ -32,3 +32,3 @@ authors = [{ name = "Murad Akhundov", email = "akhundov1murad@gmail.com" }]

[project.scripts]
typeid = "typeid.cli:cli"
typeid = "typeid.cli.main:cli"

@@ -53,4 +53,17 @@ [dependency-groups]

"maturin>=1.5; platform_system != 'Windows'",
"pre-commit>=4.5.1",
"httpx>=0.28.1",
"pydantic>=2,<3",
]
[tool.black]
line-length = 119
[tool.ruff]
line-length = 119
target-version = "py311"
[tool.mypy]
pretty = true
[build-system]

@@ -57,0 +70,0 @@ requires = ["maturin>=1.5"]

# TypeID Python
[![Run Tests](https://github.com/akhundMurad/typeid-python/actions/workflows/test.yml/badge.svg)](https://github.com/akhundMurad/typeid-python/actions/workflows/test.yml)
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/typeid-python?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/typeid-python)
[![PyPI - Version](https://img.shields.io/pypi/v/typeid-python?color=green)](https://pypi.org/project/typeid-python/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/typeid-python?color=green)](https://pypi.org/project/typeid-python/)
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/typeid-python?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/typeid-python)
![GitHub License](https://img.shields.io/github/license/akhundMurad/typeid-python)
> [!WARNING]
> `main` may contain unreleased changes. For stable usage, use the latest release tag.

@@ -19,3 +21,3 @@ A **high-performance Python implementation of [TypeIDs](https://github.com/jetpack-io/typeid)** — type-safe,

This library provides a Python package with optional Rust acceleration.
This library provides a Python package with Rust acceleration.

@@ -25,7 +27,8 @@ ## Key features

- ✅ UUIDv7-based, time-sortable identifiers
- ✅ Type-safe prefixes (`user_`, `order_`, …)
- ✅ Schema-based ID explanations (JSON / YAML)
- ✅ Fast generation & parsing (Rust-accelerated)
- ✅ Multiple integrations (Pydantic, FastAPI, ...)
- ✅ 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

@@ -75,4 +78,4 @@

```console
$ pip install typeid-python[yaml] # YAML schema support
$ pip install typeid-python[cli] # CLI tools
$ pip install typeid-python[yaml] # YAML schema support
$ pip install typeid-python[cli] # CLI tools
```

@@ -155,2 +158,27 @@

## Framework integrations
TypeID is **framework-agnostic by design**.
Integrations are provided as optional adapters, installed explicitly and kept separate from the core.
### Available integrations
* **Pydantic (v2)**
Native field type with validation and JSON Schema support.
```python
from typing import Literal
from pydantic import BaseModel
from typeid.integrations.pydantic import TypeIDField
class User(BaseModel):
id: TypeIDField[Literal["user"]]
```
* **FastAPI** (Coming Soon 🚧)
* **SQLAlchemy** (Coming Soon 🚧)
All integrations are **opt-in via extras** and never affect the core package.
## ✨ `typeid explain` — understand any ID

@@ -204,3 +232,2 @@

* **Non-breaking**: stable APIs
* **Optional acceleration**: Rust is opt-in
* **Lazy evaluation**: work is done only when needed

@@ -207,0 +234,0 @@ * **Explainability**: identifiers carry meaning

@@ -157,3 +157,3 @@ # This file is automatically @generated by Cargo.

name = "typeid-base32"
version = "0.3.7"
version = "0.3.8"
dependencies = [

@@ -160,0 +160,0 @@ "pyo3",

[package]
name = "typeid-base32"
version = "0.3.7"
version = "0.3.8"
edition = "2021"

@@ -5,0 +5,0 @@

@@ -1,3 +0,4 @@

from .factory import TypeIDFactory, cached_typeid_factory, typeid_factory
from .typeid import TypeID, from_string, from_uuid, get_prefix_and_suffix
from typeid.core.factory import TypeIDFactory, cached_typeid_factory, typeid_factory
from typeid.core.typeid import TypeID, from_string, from_uuid
from typeid.core.parsing import get_prefix_and_suffix

@@ -4,0 +5,0 @@ __all__ = (

@@ -1,9 +0,13 @@

from typeid._base32 import encode as _encode_rust, decode as _decode_rust # type: ignore
# Compatibility shim.
#
# This module exists to preserve backward compatibility with earlier
# versions of the library. Public symbols are re-exported from their
# current implementation locations.
#
# New code should prefer importing from the canonical modules, but
# existing imports will continue to work.
from typeid.codecs.base32 import encode, decode
def encode(src: bytes) -> str:
return _encode_rust(src)
def decode(s: str) -> bytes:
return _decode_rust(s)
__all__ = ("encode", "decode")

@@ -1,5 +0,13 @@

SUFFIX_LEN = 26
# Compatibility shim.
#
# This module exists to preserve backward compatibility with earlier
# versions of the library. Public symbols are re-exported from their
# current implementation locations.
#
# New code should prefer importing from the canonical modules, but
# existing imports will continue to work.
PREFIX_MAX_LEN = 63
from typeid.core.constants import PREFIX_MAX_LEN, SUFFIX_LEN, ALPHABET
ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz"
__all__ = ("PREFIX_MAX_LEN", "SUFFIX_LEN", "ALPHABET")

@@ -1,14 +0,18 @@

class TypeIDException(Exception):
...
# Compatibility shim.
#
# This module exists to preserve backward compatibility with earlier
# versions of the library. Public symbols are re-exported from their
# current implementation locations.
#
# New code should prefer importing from the canonical modules, but
# existing imports will continue to work.
from typeid.core.errors import (
TypeIDException,
PrefixValidationException,
SuffixValidationException,
InvalidTypeIDStringException,
)
class PrefixValidationException(TypeIDException):
...
class SuffixValidationException(TypeIDException):
...
class InvalidTypeIDStringException(TypeIDException):
...
__all__ = ("TypeIDException", "PrefixValidationException", "SuffixValidationException", "InvalidTypeIDStringException")

@@ -1,40 +0,13 @@

from dataclasses import dataclass
from functools import lru_cache
from typing import Callable
# Compatibility shim.
#
# This module exists to preserve backward compatibility with earlier
# versions of the library. Public symbols are re-exported from their
# current implementation locations.
#
# New code should prefer importing from the canonical modules, but
# existing imports will continue to work.
from .typeid import TypeID
from typeid.core.factory import TypeIDFactory, typeid_factory, cached_typeid_factory
@dataclass(frozen=True, slots=True)
class TypeIDFactory:
"""
Callable object that generates TypeIDs with a fixed prefix.
Example:
user_id = TypeIDFactory("user")()
"""
prefix: str
def __call__(self) -> TypeID:
return TypeID(self.prefix)
def typeid_factory(prefix: str) -> Callable[[], TypeID]:
"""
Return a zero-argument callable that generates TypeIDs with a fixed prefix.
Example:
user_id = typeid_factory("user")()
"""
return TypeIDFactory(prefix)
@lru_cache(maxsize=256)
def cached_typeid_factory(prefix: str) -> Callable[[], TypeID]:
"""
Same as typeid_factory, but caches factories by prefix.
Use this if you create factories repeatedly at runtime.
"""
return TypeIDFactory(prefix)
__all__ = ("TypeIDFactory", "typeid_factory", "cached_typeid_factory")

@@ -1,316 +0,14 @@

from datetime import datetime, timezone
import warnings
import uuid_utils
from typing import Generic, Optional, TypeVar
# Compatibility shim.
#
# This module exists to preserve backward compatibility with earlier
# versions of the library. Public symbols are re-exported from their
# current implementation locations.
#
# New code should prefer importing from the canonical modules, but
# existing imports will continue to work.
from typeid import base32
from typeid.errors import InvalidTypeIDStringException
from typeid.validation import validate_prefix, validate_suffix_and_decode
from typeid.core.typeid import PrefixT, TypeID, from_string, from_uuid
from typeid.core.parsing import get_prefix_and_suffix
PrefixT = TypeVar("PrefixT", bound=str)
def _uuid_from_bytes_v7(uuid_bytes: bytes) -> uuid_utils.UUID:
"""
Construct a UUID object from bytes.
"""
uuid_int = int.from_bytes(uuid_bytes, "big")
return uuid_utils.UUID(int=uuid_int)
class TypeID(Generic[PrefixT]):
"""
A TypeID is a human-meaningful, UUID-backed identifier.
A TypeID is rendered as:
<prefix>_<suffix> or just <suffix> (when prefix is None/empty)
- **prefix**: optional semantic label (e.g. "user", "order"). It is *not* part of the UUID.
Prefixes are validated for allowed characters/shape (see `validate_prefix`).
- **suffix**: a compact, URL-safe Base32 encoding of a UUID (UUIDv7 by default).
Suffixes are validated structurally (see `validate_suffix`).
Design notes:
- A TypeID is intended to be safe to store as a string (e.g. in logs / URLs).
- The underlying UUID can always be recovered via `.uuid`.
- Ordering (`>`, `>=`) is based on lexicographic order of the string representation,
which corresponds to time-ordering if the UUID version is time-sortable (UUIDv7).
Type parameters:
PrefixT: a type-level constraint for the prefix (often `str` or a Literal).
"""
__slots__ = ("_prefix", "_suffix", "_uuid_bytes", "_uuid", "_str")
def __init__(self, prefix: Optional[PrefixT] = None, suffix: Optional[str] = None) -> None:
"""
Create a new TypeID.
If `suffix` is not provided, a new UUIDv7 is generated and encoded as Base32.
If `prefix` is provided, it is validated.
Args:
prefix: Optional prefix. If None, the TypeID has no prefix and its string
form will be just the suffix. If provided, it must pass `validate_prefix`.
suffix: Optional Base32-encoded UUID string. If None, a new UUIDv7 is generated.
Raises:
InvalidTypeIDStringException (or another project-specific exception):
If `suffix` is invalid, or if `prefix` is invalid.
"""
# Validate prefix early (cheap) so failures don't do extra work
if prefix:
validate_prefix(prefix=prefix)
self._prefix: Optional[PrefixT] = prefix
self._str: Optional[str] = None
self._uuid: Optional[uuid_utils.UUID] = None
self._uuid_bytes: Optional[bytes] = None
if not suffix:
# generate uuid (fast path)
u = uuid_utils.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
self._suffix = suffix
@classmethod
def from_string(cls, string: str) -> "TypeID":
"""
Parse a TypeID from its string form.
The input can be either:
- "<prefix>_<suffix>"
- "<suffix>" (prefix-less)
Args:
string: String representation of a TypeID.
Returns:
A `TypeID` instance.
Raises:
InvalidTypeIDStringException (or another project-specific exception):
If the string cannot be split/parsed or if the extracted parts are invalid.
"""
# Split into (prefix, suffix) according to project rules.
prefix, suffix = get_prefix_and_suffix(string=string)
return cls(suffix=suffix, prefix=prefix)
@classmethod
def from_uuid(cls, suffix: uuid_utils.UUID, prefix: Optional[PrefixT] = None) -> "TypeID":
"""
Construct a TypeID from an existing UUID.
This is useful when you store UUIDs in a database but want to expose
TypeIDs at the application boundary.
Args:
suffix: UUID value to encode into the TypeID suffix.
prefix: Optional prefix to attach (validated if provided).
Returns:
A `TypeID` whose `.uuid` equals the provided UUID.
"""
# 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
obj._str = None
return obj
@property
def suffix(self) -> str:
"""
The Base32-encoded UUID portion of the TypeID (always present).
Notes:
- This is the identity-carrying part.
- It is validated at construction time.
"""
return self._suffix
@property
def prefix(self) -> str:
"""
The prefix portion of the TypeID, as a string.
Returns:
The configured prefix, or "" if the TypeID is prefix-less.
Notes:
- Empty string is the *presentation* of "no prefix". Internally, `_prefix`
remains Optional to preserve the distinction between None and a real value.
"""
return self._prefix or ""
@property
def uuid(self) -> uuid_utils.UUID:
"""
The UUID represented by this TypeID.
Returns:
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
@property
def uuid_bytes(self) -> bytes:
"""
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.
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:
"""
Render the TypeID into its canonical string representation.
Returns:
"<prefix>_<suffix>" if prefix is present, otherwise "<suffix>".
"""
# cache string representation; helps workflow + comparisons
s = self._str
if s is not None:
return s
if self.prefix:
s = f"{self.prefix}_{self.suffix}"
else:
s = self.suffix
self._str = s
return s
def __repr__(self):
"""
Developer-friendly representation.
Uses a constructor-like form to make debugging and copy/paste easier.
"""
return "%s.from_string(%r)" % (self.__class__.__name__, str(self))
def __eq__(self, value: object) -> bool:
"""
Equality based on prefix and suffix.
Notes:
- Two TypeIDs are considered equal if both their string components match.
- This is stricter than "same UUID" because prefix is part of the public ID.
"""
if not isinstance(value, TypeID):
return False
return value.prefix == self.prefix and value.suffix == self.suffix
def __gt__(self, other) -> bool:
"""
Compare TypeIDs by lexicographic order of their string form.
This is useful because TypeID suffixes based on UUIDv7 are time-sortable,
so string order typically corresponds to creation time order (within a prefix).
Returns:
True/False if `other` is a TypeID, otherwise NotImplemented.
"""
if isinstance(other, TypeID):
return str(self) > str(other)
return NotImplemented
def __ge__(self, other) -> bool:
"""
Compare TypeIDs by lexicographic order of their string form (>=).
See `__gt__` for rationale and notes.
"""
if isinstance(other, TypeID):
return str(self) >= str(other)
return NotImplemented
def __hash__(self) -> int:
"""
Hash based on (prefix, suffix), allowing TypeIDs to be used as dict keys / set members.
"""
return hash((self.prefix, self.suffix))
def from_string(string: str) -> TypeID:
warnings.warn("Consider TypeID.from_string instead.", DeprecationWarning)
return TypeID.from_string(string=string)
def from_uuid(suffix: uuid_utils.UUID, prefix: Optional[str] = None) -> TypeID:
warnings.warn("Consider TypeID.from_uuid instead.", DeprecationWarning)
return TypeID.from_uuid(suffix=suffix, prefix=prefix)
def get_prefix_and_suffix(string: str) -> tuple:
parts = string.rsplit("_", 1)
# When there's no underscore in the string.
if len(parts) == 1:
if parts[0].strip() == "":
raise InvalidTypeIDStringException(f"Invalid TypeID: {string}")
return None, parts[0]
# When there is an underscore, unpack prefix and suffix.
prefix, suffix = parts
if prefix.strip() == "" or suffix.strip() == "":
raise InvalidTypeIDStringException(f"Invalid TypeID: {string}")
return prefix, suffix
__all__ = ("PrefixT", "TypeID", "from_string", "from_uuid", "get_prefix_and_suffix")

@@ -1,38 +0,13 @@

import re
# Compatibility shim.
#
# This module exists to preserve backward compatibility with earlier
# versions of the library. Public symbols are re-exported from their
# current implementation locations.
#
# New code should prefer importing from the canonical modules, but
# existing imports will continue to work.
from typeid import base32
from typeid.constants import SUFFIX_LEN, ALPHABET
from typeid.errors import PrefixValidationException, SuffixValidationException
from typeid.core.validation import validate_prefix, validate_suffix_and_decode
_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:
# Use fullmatch (anchored) and precompiled regex
if not _PREFIX_RE.fullmatch(prefix or ""):
raise PrefixValidationException(f"Invalid prefix: {prefix}.")
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 (
len(suffix) != SUFFIX_LEN
or suffix == ""
or " " in suffix
or (not suffix.isdigit() and not suffix.islower())
or any([symbol not in ALPHABET for symbol in suffix])
or suffix[0] > "7"
):
raise SuffixValidationException(f"Invalid suffix: {suffix}.")
try:
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
__all__ = ("validate_prefix", "validate_suffix_and_decode")
from pathlib import Path
from typing import Optional
import click
from uuid_utils import UUID
from typeid import TypeID, base32, from_uuid, get_prefix_and_suffix
from typeid.explain.discovery import discover_schema_path
from typeid.explain.engine import explain as explain_engine
from typeid.explain.formatters import format_explanation_json, format_explanation_pretty
from typeid.explain.registry import load_registry, make_lookup
@click.group()
def cli():
# Root CLI command group.
# This acts as the entry point for all subcommands.
pass
@cli.command()
@click.option("-p", "--prefix")
def new(prefix: Optional[str] = None) -> None:
"""
Generate a new TypeID.
If a prefix is provided, it will be validated and included in the output.
If no prefix is provided, a prefix-less TypeID is generated.
"""
typeid = TypeID(prefix=prefix)
click.echo(str(typeid))
@cli.command()
@click.argument("uuid")
@click.option("-p", "--prefix")
def encode(uuid: str, prefix: Optional[str] = None) -> None:
"""
Encode an existing UUID into a TypeID.
This command is intended for cases where UUIDs already exist
(e.g. stored in a database) and need to be represented as TypeIDs.
"""
uuid_obj = UUID(uuid)
typeid = from_uuid(suffix=uuid_obj, prefix=prefix)
click.echo(str(typeid))
@cli.command()
@click.argument("encoded")
def decode(encoded: str) -> None:
"""
Decode a TypeID into its components.
This extracts:
- the prefix (if any)
- the underlying UUID
This command is primarily intended for inspection and debugging.
"""
prefix, suffix = get_prefix_and_suffix(encoded)
decoded_bytes = bytes(base32.decode(suffix))
uuid = UUID(bytes=decoded_bytes)
click.echo(f"type: {prefix}")
click.echo(f"uuid: {uuid}")
@cli.command()
@click.argument("encoded")
@click.option(
"--schema",
"schema_path",
type=click.Path(exists=True, dir_okay=False, path_type=str),
required=False,
help="Path to TypeID schema file (JSON, or YAML if PyYAML is installed). "
"If omitted, TypeID will try to discover a schema automatically.",
)
@click.option(
"--json",
"as_json",
is_flag=True,
help="Output machine-readable JSON.",
)
@click.option(
"--no-schema",
is_flag=True,
help="Disable schema lookup (derived facts only).",
)
@click.option(
"--no-links",
is_flag=True,
help="Disable link template rendering.",
)
def explain(
encoded: str,
schema_path: Optional[str],
as_json: bool,
no_schema: bool,
no_links: bool,
) -> None:
"""
Explain a TypeID: parse/validate it, derive facts (uuid, created_at),
and optionally enrich explanation from a user-provided schema.
"""
enable_schema = not no_schema
enable_links = not no_links
schema_lookup = None
warnings: list[str] = []
# Load schema (optional)
if enable_schema:
resolved_path = None
if schema_path:
resolved_path = schema_path
else:
discovery = discover_schema_path()
if discovery.path is not None:
resolved_path = str(discovery.path)
# If env var was set but invalid, discovery returns source info;
# we keep CLI robust and simply proceed without schema.
if resolved_path:
result = load_registry(Path(resolved_path))
if result.registry is not None:
schema_lookup = make_lookup(result.registry)
else:
if result.error is not None:
warnings.append(f"Schema load failed: {result.error.message}")
# Build explanation (never raises on normal errors)
exp = explain_engine(
encoded,
schema_lookup=schema_lookup,
enable_schema=enable_schema,
enable_links=enable_links,
)
# Surface schema-load warnings (if any)
if warnings:
exp.warnings.extend(warnings)
# Print
if as_json:
click.echo(format_explanation_json(exp))
else:
click.echo(format_explanation_pretty(exp))
if __name__ == "__main__":
cli()