diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b1a4af0..ffad8b8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -23,7 +23,7 @@ jobs: - name: Sync dependencies (locked) run: | - uv sync --locked --all-groups + uv sync --locked --all-groups --all-extras - name: Build & install native extension (maturin develop) uses: PyO3/maturin-action@v1 @@ -55,7 +55,7 @@ jobs: - name: Sync dependencies (locked) run: | - uv sync --locked --all-groups + uv sync --locked --all-groups --all-extras - name: Build & install native extension (maturin develop) uses: PyO3/maturin-action@v1 diff --git a/README.md b/README.md index 3e47bf3..08fcdc4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # 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. A **high-performance Python implementation of [TypeIDs](https://github.com/jetpack-io/typeid)** — type-safe, sortable identifiers based on **UUIDv7**. @@ -21,11 +23,12 @@ This library provides a Python package with Rust acceleration. ## Key features - ✅ UUIDv7-based, time-sortable identifiers -- ✅ Type-safe prefixes (`user_`, `order_`, …) -- ✅ Human-readable and URL-safe +- ✅ 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 - ✅ CLI tools (`new`, `encode`, `decode`, `explain`) -- ✅ Schema-based ID explanations (JSON / YAML) - ✅ Fully offline, no external services ## Performance @@ -70,8 +73,9 @@ Included: ### Other optional extras ```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 +$ pip install typeid-python[pydantic] # Pydantic integration ``` Extras are **strictly optional**. @@ -149,6 +153,45 @@ Encode: $ typeid encode 0188bac7-4afa-78aa-bc3b-bd1eef28d881 --prefix user ``` +## 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. + + ```bash + pip install typeid-python[pydantic] + ``` + + ```python + from typing import Literal + from pydantic import BaseModel + from typeid.integrations.pydantic import TypeIDField + + class User(BaseModel): + id: TypeIDField[Literal["user"]] + ``` + +* **FastAPI** + Works automatically via Pydantic (request/response models, OpenAPI). + + ```bash + pip install typeid-python[fastapi] + ``` + +* **SQLAlchemy** + Column types for storing TypeIDs (typically as strings). + + ```bash + pip install typeid-python[sqlalchemy] + ``` + +All integrations are **opt-in via extras** and never affect the core package. + ## ✨ `typeid explain` — understand any ID ```console diff --git a/docs/integrations/index.md b/docs/integrations/index.md new file mode 100644 index 0000000..88fd536 --- /dev/null +++ b/docs/integrations/index.md @@ -0,0 +1,72 @@ +# Framework Integrations + +TypeID is designed to be framework-agnostic. +The core package does not depend on any web framework, ORM, or serialization library. + +Integrations are provided as **optional** adapters that translate between TypeID and specific ecosystems. +They are installed explicitly and never affect the core unless imported. + +--- + +## Available integrations + +### Pydantic (v2) + +Native support for using TypeID in Pydantic v2 models. + +* Prefix-aware validation +* Accepts `str` and `TypeID` +* Serializes as string +* Clean JSON Schema / OpenAPI output + +```bash +pip install typeid-python[pydantic] +``` + +See: [Pydantic v2](https://akhundmurad.github.io/typeid-python/integrations/pydantic/) documentation page for details and examples. + +### FastAPI + +FastAPI builds on Pydantic v2, so TypeID works automatically in: + +* request models +* response models +* OpenAPI schemas + +No separate FastAPI-specific adapter is required. + +```bash +pip install typeid-python[fastapi] +``` + +See: [Pydantic v2](https://akhundmurad.github.io/typeid-python/integrations/fastapi/) documentation page for details and examples. + +### SQLAlchemy + +Column types for storing TypeIDs in relational databases. + +Typical usage stores the full TypeID string (`prefix_suffix`) for clarity and debuggability. + +```bash +pip install typeid-python[sqlalchemy] +``` + +See: [Pydantic v2](https://akhundmurad.github.io/typeid-python/integrations/sqlalchemy/) documentation page for details and examples. + +## Design notes + +* The TypeID core never imports framework code +* Validation rules live in the core, not in adapters +* Integrations are thin and easy to replace or extend + +This keeps the library stable even as frameworks evolve. + +## Adding new integrations + +If you want to integrate TypeID with another framework: + +* keep the adapter small, +* avoid duplicating validation logic, +* depend on the TypeID core as the single source of truth. + +Community integrations are welcome. diff --git a/docs/integrations/pydantic.md b/docs/integrations/pydantic.md new file mode 100644 index 0000000..809e911 --- /dev/null +++ b/docs/integrations/pydantic.md @@ -0,0 +1,179 @@ +# Pydantic v2 integration + +TypeID ships with an **optional Pydantic v2 adapter**. +It lets you use `TypeID` in Pydantic models without pulling Pydantic into the TypeID core. + +The adapter: + +* validates values using the TypeID core, +* optionally enforces a fixed prefix, +* serializes TypeIDs as strings, +* exposes sensible JSON Schema metadata. + +--- + +## Installation + +```bash +pip install typeid-python[pydantic] +``` + +This installs the latest version of Pydantic v2. + +## Basic usage + +Use `TypeIDField` with a fixed prefix. + +```python +from typing import Literal +from pydantic import BaseModel +from typeid.integrations.pydantic import TypeIDField + +class User(BaseModel): + id: TypeIDField[Literal["user"]] + +u = User(id="user_01ke82dtesfn9bjcrzyzz54ya9") +assert str(u.id) == "user_01ke82dtesfn9bjcrzyzz54ya9" +``` + +## Accepted inputs + +You can pass either a string or a `TypeID` instance. + +```python +from typing import Literal +from pydantic import BaseModel +from typeid.integrations.pydantic import TypeIDField + +class User(BaseModel): + id: TypeIDField[Literal["user"]] + +u = User(id="user_01ke82dtesfn9bjcrzyzz54ya9") +assert u.id is not None +``` + +```python +from typing import Literal +from pydantic import BaseModel +from typeid import TypeID +from typeid.integrations.pydantic import TypeIDField + +class User(BaseModel): + id: TypeIDField[Literal["user"]] + +tid = TypeID.from_string("user_01ke82dtesfn9bjcrzyzz54ya9") +u = User(id=tid) + +assert u.id == tid +``` + +In both cases, `id` is stored as a `TypeID` object inside the model. + +## Prefix validation + +The prefix in `TypeIDField[...]` is enforced. + +```python +import pytest +from typing import Literal +from pydantic import BaseModel, ValidationError +from typeid.integrations.pydantic import TypeIDField + +class Order(BaseModel): + id: TypeIDField[Literal["order"]] + +with pytest.raises(ValidationError): + Order(id="user_01ke82dtesfn9bjcrzyzz54ya9") +``` + +This fails with a validation error because the prefix does not match. + +This is useful when you want the model itself to encode domain meaning +(e.g. *this field must be a user ID, not just any ID*). + +## Serialization + +When exporting a model, TypeIDs are always serialized as strings. + +```python +from typing import Literal +from pydantic import BaseModel +from typeid.integrations.pydantic import TypeIDField + +class User(BaseModel): + id: TypeIDField[Literal["user"]] + +u = User(id="user_01ke82dtesfn9bjcrzyzz54ya9") +data = u.model_dump(mode="json") + +assert data == {"id": "user_01ke82dtesfn9bjcrzyzz54ya9"} +``` + +```python +from typing import Literal +from pydantic import BaseModel +from typeid.integrations.pydantic import TypeIDField + +class User(BaseModel): + id: TypeIDField[Literal["user"]] + +u = User(id="user_01ke82dtesfn9bjcrzyzz54ya9") +json_data = u.model_dump_json() + +assert json_data == '{"id":"user_01ke82dtesfn9bjcrzyzz54ya9"}' +``` + +This keeps JSON output simple and predictable. + +## JSON Schema / OpenAPI + +The generated schema looks roughly like this: + +```yaml +id: + type: string + format: typeid + description: TypeID with prefix 'user' + examples: + - user_01ke82dtesfn9bjcrzyzz54ya9 +``` + +Notes: + +* The schema does not hard-code internal regex details. +* Actual validation is handled by the TypeID core. +* The schema is meant to document intent, not re-implement parsing rules. + +## Why `Literal["user"]`? + +The recommended form is: + +```text +TypeIDField[Literal["user"]] +``` + +This works cleanly with: + +* Ruff +* Pyright / MyPy +* IDE type checkers + +Using `Literal` makes the prefix a real compile-time constant and avoids +annotation edge cases. + +## FastAPI + +FastAPI uses Pydantic v2, so no extra integration is needed. + +TypeID fields work automatically in request and response models, +including OpenAPI output, as soon as you use them in a Pydantic model. + +## Design notes + +* The TypeID core does not import Pydantic. +* All framework-specific code lives in `typeid.integrations.pydantic`. +* Parsing and validation rules live in the core, not in the adapter. + +This keeps the integration small and easy to maintain. + +*That’s it — no magic, no hidden behavior.* diff --git a/docs/reference/cli.md b/docs/reference/cli.md index f1554a0..3ac95cf 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -1,3 +1,3 @@ # CLI -::: typeid.cli +::: typeid.cli.main diff --git a/docs/reference/factory.md b/docs/reference/factory.md index d5be1b4..e2da7bb 100644 --- a/docs/reference/factory.md +++ b/docs/reference/factory.md @@ -1,3 +1,3 @@ # Factory -::: typeid.factory +::: typeid.core.factory diff --git a/docs/reference/integrations/pydantic/v2.md b/docs/reference/integrations/pydantic/v2.md new file mode 100644 index 0000000..49792ab --- /dev/null +++ b/docs/reference/integrations/pydantic/v2.md @@ -0,0 +1,3 @@ +# Pydantic V2 Integration + +::: typeid.integrations.pydantic.v2 diff --git a/docs/reference/typeid.md b/docs/reference/typeid.md index 522db22..fcd84c1 100644 --- a/docs/reference/typeid.md +++ b/docs/reference/typeid.md @@ -1,3 +1,3 @@ # TypeID -::: typeid.typeid.TypeID +::: typeid.TypeID diff --git a/mkdocs.yml b/mkdocs.yml index ea01226..4ff7876 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,6 +15,22 @@ theme: - search.share icon: repo: fontawesome/brands/github + palette: + # Light mode + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/toggle-switch-off-outline + name: Switch to dark mode + + # Dark mode + - scheme: slate + primary: blue + accent: blue + toggle: + icon: material/toggle-switch + name: Switch to light mode plugins: - search @@ -42,11 +58,17 @@ nav: - Index: index.md - Quickstart: quickstart.md - Performance: performance.md + - Concepts: concepts.md + - Explain: explain.md + - Framework Integrations: + - Overview: integrations/index.md + - Pydantic: integrations/pydantic.md - Reference: - TypeID: reference/typeid.md - Explain: reference/explain.md - CLI: reference/cli.md - Factory: reference/factory.md - - Concepts: concepts.md - - Explain: explain.md + - Framework Integrations: + - Pydantic: + - v2: reference/integrations/pydantic/v2.md - Contributing: contributing.md diff --git a/pyproject.toml b/pyproject.toml index 4e076cc..139ce5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ dependencies = ["uuid-utils>=0.12.0"] [project.optional-dependencies] cli = ["click"] yaml = ["PyYAML"] +pydantic = ["pydantic>=2,<3"] [project.urls] Homepage = "https://github.com/akhundMurad/typeid-python" @@ -29,7 +30,7 @@ Repository = "https://github.com/akhundMurad/typeid-python" "Bug Tracker" = "https://github.com/akhundMurad/typeid-python/issues" [project.scripts] -typeid = "typeid.cli:cli" +typeid = "typeid.cli.main:cli" [dependency-groups] dev = [ diff --git a/tests/integrations/__init__.py b/tests/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integrations/test_pydantic.py b/tests/integrations/test_pydantic.py new file mode 100644 index 0000000..8a50be5 --- /dev/null +++ b/tests/integrations/test_pydantic.py @@ -0,0 +1,36 @@ +from typing import Literal +import pytest +from pydantic import BaseModel, ValidationError + +from typeid.integrations.pydantic import TypeIDField +from typeid import TypeID + + +USER_TYPEID_STR = str(TypeID("user")) +ORDER_TYPEID_STR = str(TypeID("order")) + + +class M(BaseModel): + id: TypeIDField[Literal["user"]] + + +def test_accepts_str(): + m = M(id=USER_TYPEID_STR) + assert isinstance(m.id, TypeID) + + +def test_accepts_typeid_instance(): + tid = TypeID.from_string(USER_TYPEID_STR) + m = M(id=tid) + assert m.id == tid + + +def test_prefix_mismatch(): + with pytest.raises(ValidationError): + M(id=ORDER_TYPEID_STR) + + +def test_json_serializes_as_string(): + m = M(id=USER_TYPEID_STR) + data = m.model_dump_json() + assert '"id":"' in data diff --git a/tests/test_base32.py b/tests/test_base32.py index 4b4cb36..2d29dd8 100644 --- a/tests/test_base32.py +++ b/tests/test_base32.py @@ -1,4 +1,4 @@ -from typeid.base32 import decode, encode +from typeid.codecs.base32 import decode, encode def test_encode_decode_logic() -> None: diff --git a/tests/test_factory.py b/tests/test_factory.py index 57e997f..04b1915 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -1,7 +1,7 @@ import pytest from typeid import TypeID, cached_typeid_factory, typeid_factory -from typeid.errors import PrefixValidationException +from typeid.core.errors import PrefixValidationException def test_typeid_factory_generates_typeid_with_prefix(): diff --git a/tests/test_spec.py b/tests/test_spec.py index 49d3a7a..57a6723 100644 --- a/tests/test_spec.py +++ b/tests/test_spec.py @@ -2,7 +2,7 @@ from uuid_utils import UUID from typeid import TypeID -from typeid.errors import TypeIDException +from typeid.core.errors import TypeIDException def test_invalid_spec(invalid_spec: list) -> None: diff --git a/tests/test_typeid.py b/tests/test_typeid.py index 835cbbb..0a9bcf4 100644 --- a/tests/test_typeid.py +++ b/tests/test_typeid.py @@ -4,7 +4,7 @@ import uuid_utils from typeid import TypeID -from typeid.errors import SuffixValidationException +from typeid.core.errors import SuffixValidationException def test_default_suffix() -> None: diff --git a/tests/test_validation.py b/tests/test_validation.py index cf71aa6..19a5973 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -2,8 +2,8 @@ from uuid_utils import uuid7 from typeid import base32 -from typeid.errors import PrefixValidationException, SuffixValidationException -from typeid.validation import validate_prefix, validate_suffix_and_decode +from typeid.core.errors import PrefixValidationException, SuffixValidationException +from typeid.core.validation import validate_prefix, validate_suffix_and_decode def test_validate_correct_prefix() -> None: diff --git a/typeid/__init__.py b/typeid/__init__.py index b5a0795..4af1c29 100644 --- a/typeid/__init__.py +++ b/typeid/__init__.py @@ -1,5 +1,6 @@ -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 __all__ = ( "TypeID", diff --git a/typeid/base32.py b/typeid/base32.py index 94a7061..5104903 100644 --- a/typeid/base32.py +++ b/typeid/base32.py @@ -1,9 +1,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") diff --git a/typeid/cli/__init__.py b/typeid/cli/__init__.py new file mode 100644 index 0000000..367fba0 --- /dev/null +++ b/typeid/cli/__init__.py @@ -0,0 +1,5 @@ +from typeid.cli.main import cli + + +if __name__ == "__main__": + cli() diff --git a/typeid/cli.py b/typeid/cli/main.py similarity index 95% rename from typeid/cli.py rename to typeid/cli/main.py index b6c9707..873428a 100644 --- a/typeid/cli.py +++ b/typeid/cli/main.py @@ -4,7 +4,9 @@ import click from uuid_utils import UUID -from typeid import TypeID, base32, from_uuid, get_prefix_and_suffix +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 @@ -42,7 +44,7 @@ def encode(uuid: str, prefix: Optional[str] = None) -> None: (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) + typeid = TypeID.from_uuid(suffix=uuid_obj, prefix=prefix) click.echo(str(typeid)) @@ -151,7 +153,3 @@ def explain( click.echo(format_explanation_json(exp)) else: click.echo(format_explanation_pretty(exp)) - - -if __name__ == "__main__": - cli() diff --git a/typeid/codecs/__init__.py b/typeid/codecs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/typeid/codecs/base32.py b/typeid/codecs/base32.py new file mode 100644 index 0000000..ce2c427 --- /dev/null +++ b/typeid/codecs/base32.py @@ -0,0 +1,90 @@ +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) diff --git a/typeid/constants.py b/typeid/constants.py index af81b48..7d0f3dd 100644 --- a/typeid/constants.py +++ b/typeid/constants.py @@ -1,5 +1,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") diff --git a/typeid/core/__init__.py b/typeid/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/typeid/core/constants.py b/typeid/core/constants.py new file mode 100644 index 0000000..af81b48 --- /dev/null +++ b/typeid/core/constants.py @@ -0,0 +1,5 @@ +SUFFIX_LEN = 26 + +PREFIX_MAX_LEN = 63 + +ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz" diff --git a/typeid/core/errors.py b/typeid/core/errors.py new file mode 100644 index 0000000..054752f --- /dev/null +++ b/typeid/core/errors.py @@ -0,0 +1,14 @@ +class TypeIDException(Exception): + ... + + +class PrefixValidationException(TypeIDException): + ... + + +class SuffixValidationException(TypeIDException): + ... + + +class InvalidTypeIDStringException(TypeIDException): + ... diff --git a/typeid/core/factory.py b/typeid/core/factory.py new file mode 100644 index 0000000..3d1d353 --- /dev/null +++ b/typeid/core/factory.py @@ -0,0 +1,40 @@ +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) diff --git a/typeid/core/parsing.py b/typeid/core/parsing.py new file mode 100644 index 0000000..d66da3c --- /dev/null +++ b/typeid/core/parsing.py @@ -0,0 +1,18 @@ +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 diff --git a/typeid/core/typeid.py b/typeid/core/typeid.py new file mode 100644 index 0000000..f322caf --- /dev/null +++ b/typeid/core/typeid.py @@ -0,0 +1,299 @@ +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: + + _ or just (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-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: + "_" if prefix is present, otherwise "". + """ + # 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) diff --git a/typeid/core/validation.py b/typeid/core/validation.py new file mode 100644 index 0000000..8271717 --- /dev/null +++ b/typeid/core/validation.py @@ -0,0 +1,38 @@ +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 diff --git a/typeid/errors.py b/typeid/errors.py index 054752f..10285a6 100644 --- a/typeid/errors.py +++ b/typeid/errors.py @@ -1,14 +1,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") diff --git a/typeid/factory.py b/typeid/factory.py index 8d907cc..8ed58a3 100644 --- a/typeid/factory.py +++ b/typeid/factory.py @@ -1,40 +1,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") diff --git a/typeid/integrations/__init__.py b/typeid/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/typeid/integrations/pydantic/__init__.py b/typeid/integrations/pydantic/__init__.py new file mode 100644 index 0000000..af2be89 --- /dev/null +++ b/typeid/integrations/pydantic/__init__.py @@ -0,0 +1,3 @@ +from .v2 import TypeIDField + +__all__ = ["TypeIDField"] diff --git a/typeid/integrations/pydantic/v2.py b/typeid/integrations/pydantic/v2.py new file mode 100644 index 0000000..552d146 --- /dev/null +++ b/typeid/integrations/pydantic/v2.py @@ -0,0 +1,161 @@ +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] diff --git a/typeid/typeid.py b/typeid/typeid.py index 10fa6cc..907ca3d 100644 --- a/typeid/typeid.py +++ b/typeid/typeid.py @@ -1,316 +1,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: - - _ or just (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-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: - "_" if prefix is present, otherwise "". - """ - # 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") diff --git a/typeid/validation.py b/typeid/validation.py index e41b372..95be0e1 100644 --- a/typeid/validation.py +++ b/typeid/validation.py @@ -1,38 +1,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") diff --git a/uv.lock b/uv.lock index 5392cd7..ab119bf 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.10, <4" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "babel" version = "2.17.0" @@ -1007,6 +1016,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -1370,6 +1512,9 @@ dependencies = [ cli = [ { name = "click" }, ] +pydantic = [ + { name = "pydantic" }, +] yaml = [ { name = "pyyaml" }, ] @@ -1398,10 +1543,11 @@ dev = [ [package.metadata] requires-dist = [ { name = "click", marker = "extra == 'cli'" }, + { name = "pydantic", marker = "extra == 'pydantic'", specifier = ">=2,<3" }, { name = "pyyaml", marker = "extra == 'yaml'" }, { name = "uuid-utils", specifier = ">=0.12.0" }, ] -provides-extras = ["cli", "yaml"] +provides-extras = ["cli", "yaml", "pydantic"] [package.metadata.requires-dev] dev = [ @@ -1433,6 +1579,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "tzdata" version = "2025.3"