diff --git a/.gitignore b/.gitignore index aa36d9e..cdf3f98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist/ /**/__pycache__/ /credentials +/.hypothesis diff --git a/pyproject.toml b/pyproject.toml index 526c81e..5d18336 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,13 @@ [project] name = "latch-data-validation" -version = "0.1.12" +version = "2.0.0" description = "Runtime type validation" authors = [{ name = "maximsmol", email = "max@latch.bio" }] -dependencies = ["opentelemetry-api>=1.15.0"] -requires-python = ">=3.11.0" +dependencies = [ + "ruff>=0.12.7", + "typing-extensions>=4.14.0", +] +requires-python = ">=3.13.0" readme = "README.md" license = { text = "CC0-1.0" } @@ -13,10 +16,10 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.uv] -dev-dependencies = ["ruff>=0.9.5"] +dev-dependencies = ["hypothesis>=6.135.4", "ruff>=0.11.13"] [tool.ruff] -target-version = "py311" +target-version = "py313" [tool.ruff.lint] preview = true @@ -36,7 +39,7 @@ extend-select = [ "ASYNC", "ASYNC1", "S", - # "BLE", # `raise x from y` does not work + "BLE", "FBT", "B", "A", @@ -56,7 +59,7 @@ extend-select = [ "PIE", "T20", "PYI", - "PT", + # "PT", "Q", "RSE", "RET", @@ -146,13 +149,12 @@ ignore = [ "PD901", - "UP040", - "SIM112", "PLC1901", "TCH002", + "TC006", ] [tool.ruff.format] @@ -160,6 +162,8 @@ preview = true skip-magic-trailing-comma = true [tool.pyright] +enableExperimentalFeatures = true + reportUnknownArgumentType = "none" reportUnknownLambdaType = "none" reportUnknownMemberType = "none" diff --git a/latch_data_validation/__init__.py b/src/latch_data_validation/__init__.py similarity index 100% rename from latch_data_validation/__init__.py rename to src/latch_data_validation/__init__.py diff --git a/latch_data_validation/data_validation.py b/src/latch_data_validation/data_validation_old.py similarity index 100% rename from latch_data_validation/data_validation.py rename to src/latch_data_validation/data_validation_old.py diff --git a/src/latch_data_validation/ok_data.py b/src/latch_data_validation/ok_data.py new file mode 100644 index 0000000..c29c47c --- /dev/null +++ b/src/latch_data_validation/ok_data.py @@ -0,0 +1,444 @@ +import sys +from collections.abc import Iterable +from keyword import iskeyword +from types import FrameType, UnionType +from typing import ( # noqa: UP035 + Annotated, + Any, + Dict, # pyright: ignore[reportDeprecated] + ForwardRef, + FrozenSet, # pyright: ignore[reportDeprecated] + Generic, + List, # pyright: ignore[reportDeprecated] + Literal, + Never, + NewType, + NotRequired, + Optional, # pyright: ignore[reportDeprecated] + Protocol, + ReadOnly, + Required, + Set, # pyright: ignore[reportDeprecated] + Tuple, # pyright: ignore[reportDeprecated] + TypeAliasType, + TypedDict, + TypeVar, + Union, # pyright: ignore[reportDeprecated] + cast, + get_args, + get_origin, +) + +from typing_extensions import NoExtraItems, TypeForm +from typing_extensions import TypedDict as TypedDict_te # noqa: UP035 + +forward_ref_frames: dict[int, FrameType] = {} + + +def patch_forward_ref() -> None: + real_init = ForwardRef.__init__ + + def __init__(self, *args, **kwargs) -> None: + cur = sys._getframe().f_back + assert cur is not None + + typing_filename = cur.f_code.co_filename + while cur is not None and cur.f_code.co_filename == typing_filename: + cur = cur.f_back + + if cur is not None: + forward_ref_frames[id(self)] = cur + + real_init(self, *args, **kwargs) + + ForwardRef.__init__ = __init__ + + +patch_forward_ref() + +TypedDictMeta = type(TypedDict("_Internal", {})) +TypedDictMeta_te = type(TypedDict_te("_Internal", {})) + + +def _render_field_name(x: str) -> str: + if x.isidentifier() and not iskeyword(x): + return x + + return repr(x) + + +def _render_type(x: TypeForm[object]) -> str: + generic_origin = get_origin(x) + generic_args = get_args(x) + + if generic_origin is Required: + generic_args = cast(tuple[TypeForm[object], ...], generic_args) + return _render_type(generic_args[0]) + + for prim in { + type(None), + type(...), + type(NotImplemented), + int, + float, + complex, + bool, + str, + bytes, + bytearray, + memoryview, + }: + if x is not prim: + continue + + return prim.__name__ + + return repr(x) + + +class DataNotOkError(RuntimeError): + def __init__(self, msg: str, *, cls: object, x: object) -> None: + self.msg: str = msg + self.cls: object = cls + self.value: object = x + + super().__init__(f"{msg}\n{cls!r}\n{x!r}") + + +class FatalDataNotOkError(DataNotOkError): ... + + +legacy_sequences: dict[type, type] = { + List: list, # noqa: UP006 # pyright: ignore[reportDeprecated] + Set: set, # noqa: UP006 # pyright: ignore[reportDeprecated] + FrozenSet: frozenset, # noqa: UP006 # pyright: ignore[reportDeprecated] +} + + +class TypedDictInstance(Protocol): + __total__: bool + __annotations__: dict[str, TypeForm[object]] + __required_keys__: frozenset[str] + __optional_keys__: frozenset[str] + __readonly_keys__: frozenset[str] + __mutable_keys__: frozenset[str] + + # https://peps.python.org/pep-0728/ + __closed__: bool + __extra_items__: TypeForm[object] | NoExtraItems + + +def ok_data[T]( + cls: TypeForm[T], + x: object, + *, + type_variables: dict[TypeVar, TypeForm[object]] | None = None, +) -> T: + # todo(maximsmol): support Annotated + if isinstance(cls, TypeVar): + if type_variables is None or cls not in type_variables: + raise DataNotOkError( + f"unknown type variable: {cls}\nknown: {type_variables!r}", cls=cls, x=x + ) + + return ok_data( + cast(TypeForm[T], type_variables[cls]), x, type_variables=type_variables + ) + + generic_origin_raw = get_origin(cls) + generic_args = get_args(cls) + + generic_origin = generic_origin_raw + if generic_origin_raw is not None and isinstance(generic_origin_raw, type): + generic_origin = legacy_sequences.get(generic_origin_raw, generic_origin_raw) + + if generic_origin is Dict: # noqa: UP006 # pyright: ignore[reportDeprecated] + generic_origin = dict + + if generic_origin is Tuple: # noqa: UP006 # pyright: ignore[reportDeprecated] + generic_origin = tuple + + if generic_origin is not None: + if generic_origin is Annotated: + return ok_data( + cast(TypeForm[T], generic_args[0]), x, type_variables=type_variables + ) + + if generic_origin is Required: + return ok_data( + cast(TypeForm[T], generic_args[0]), x, type_variables=type_variables + ) + + if generic_origin is NotRequired: + return ok_data( + cast(TypeForm[T], generic_args[0]), x, type_variables=type_variables + ) + + if generic_origin is ReadOnly: + return ok_data( + cast(TypeForm[T], generic_args[0]), x, type_variables=type_variables + ) + + if isinstance(generic_origin, TypedDictMeta | TypedDictMeta_te): + # class TypeDict(Generic[T]): + + if type_variables is None: + type_variables = {} + type_variables = type_variables.copy() + type_variables.update( + dict(zip(generic_origin.__parameters__, generic_args, strict=True)) + ) + + return ok_data( + cast(TypeForm[T], cast(object, generic_origin)), + x, + type_variables=type_variables, + ) + + # todo(maximsmol): add discriminated union support + if generic_origin is UnionType or generic_origin is Union: # pyright: ignore[reportDeprecated] + errors: list[DataNotOkError] = [] + for sub in cast(tuple[TypeForm[object], ...], generic_args): + try: + return cast(T, ok_data(sub, x, type_variables=type_variables)) + except FatalDataNotOkError: + raise + except DataNotOkError as e: + errors.append(e) + + # todo(maximsmol): attach suberrors + raise DataNotOkError("did not match any union members", cls=cls, x=x) + + if generic_origin is Optional: # pyright: ignore[reportDeprecated] + if x is None: + return cast(T, x) + + # todo(maximsmol): attach error context + return ok_data( + cast(TypeForm[T], generic_args[0]), x, type_variables=type_variables + ) + + if generic_origin is Literal: + if x not in generic_args: + raise DataNotOkError("not one of the specified literals", cls=cls, x=x) + + # todo(maximsmol): only allow supported types + + return cast(T, x) + + if any(generic_origin is x for x in legacy_sequences.values()): + if not isinstance(x, generic_origin): + raise DataNotOkError(f"not a `{generic_origin.__name__}`", cls=cls, x=x) + + generic_args = cast(tuple[TypeForm[object]], generic_args) + + if generic_origin_raw is generic_origin and isinstance( + generic_args[0], str + ): + raise FatalDataNotOkError( + f"using built-in `{generic_origin.__name__}` (PEP585) with a string (forward reference) is not supported. Make an alias using the `type` syntax from 3.12 (PEP695)", + cls=cls, + x=x, + ) + + assert isinstance(x, Iterable) + + for xx in x: + # todo(maximsmol): if the item changed (e.g. because of dataclass from dict), rebuilt the container + # todo(maximsmol): attach error context + _ = ok_data(generic_args[0], xx, type_variables=type_variables) + + return cast(T, x) + + if generic_origin is dict: # noqa: UP006 # pyright: ignore[reportDeprecated] + if not isinstance(x, dict): + raise DataNotOkError("not a `dict`", cls=cls, x=x) + + generic_args = cast(tuple[TypeForm[object], TypeForm[object]], generic_args) + + if generic_origin_raw is dict and ( + isinstance(generic_args[0], str) or isinstance(generic_args[1], str) + ): + raise FatalDataNotOkError( + "using built-in `dict` (PEP585) with a string (forward reference) is not supported. Make an alias using the `type` syntax from 3.12 (PEP695)", + cls=cls, + x=x, + ) + + for k, v in x.items(): + # todo(maximsmol): if the item changed (e.g. because of dataclass from dict), rebuilt the container + # todo(maximsmol): attach error context + _ = ok_data(generic_args[0], k, type_variables=type_variables) + _ = ok_data(generic_args[1], v, type_variables=type_variables) + + return cast(T, x) + + if generic_origin is tuple: + if not isinstance(x, tuple): + raise DataNotOkError("not a `tuple`", cls=cls, x=x) + + generic_args = cast(tuple[TypeForm[object], ...], generic_args) + + if generic_origin_raw is tuple and ( + any(isinstance(x, str) for x in generic_args) + ): + raise FatalDataNotOkError( + "using built-in `tuple` (PEP585) with a string (forward reference) is not supported. Make an alias using the `type` syntax from 3.12 (PEP695)", + cls=cls, + x=x, + ) + + if len(x) != len(generic_args): + raise DataNotOkError( + f"incorrect tuple length: {len(x)} (expected {len(generic_args)})", + cls=cls, + x=x, + ) + + for t, v in zip(generic_args, x, strict=True): + # todo(maximsmol): if the item changed (e.g. because of dataclass from dict), rebuilt the container + # todo(maximsmol): attach error context + _ = ok_data(t, v, type_variables=type_variables) + + return cast(T, x) + + raise FatalDataNotOkError( + f"unsupported type form: {generic_origin!r}[{ + ', '.join(repr(a) for a in generic_args) # pyright: ignore[reportAny] + }] ({type(cls)!r})", + cls=cls, + x=x, + ) + + # Support for `type` syntax in Python 3.12 + if isinstance(cls, TypeAliasType): + return ok_data( + cls.__value__, # pyright: ignore[reportAny] + x, + type_variables=type_variables, + ) + + # Support for old forward references + if isinstance(cls, ForwardRef): + frame = forward_ref_frames.get(id(cls)) + if frame is None: + raise FatalDataNotOkError("untraced forward reference", cls=cls, x=x) + + f_globals = frame.f_globals + f_locals = frame.f_locals + + target = f_globals.get(cls.__forward_arg__) + if target is None: + target = f_locals.get(cls.__forward_arg__) + + if target is None: + raise FatalDataNotOkError("unresolvable ForwardRef", cls=cls, x=x) + + return ok_data(cast(type[T], target), x, type_variables=type_variables) + + if isinstance(cls, NewType): + # todo(maximsmol): add context to error + return ok_data( + cast(TypeForm[T], cls.__supertype__), x, type_variables=type_variables + ) + + if not isinstance(cls, type): + raise FatalDataNotOkError(f"invalid type: {cls!r}", cls=cls, x=x) + + if cls is Any: + return cast(T, x) + + # todo(maximsmol): allow bytes, bytearray, memoryview to use the buffer protocol + # https://docs.python.org/3/c-api/buffer.html#bufferobjects + + for prim in (None, ..., NotImplemented): + if ( + cast(Any, cls) # pyright: ignore[reportExplicitAny] + is type(prim) + ): + if x is not prim: + raise DataNotOkError(f"not a `{prim}`", cls=cls, x=x) + + return cast(T, prim) + + if cls is int and (x is True or x is False): + raise DataNotOkError("not a `int`", cls=cls, x=x) + + for prim in (int, float, complex, bool, str, bytes, bytearray, memoryview): + if cls is not prim: + continue + + if not isinstance(x, prim): + raise DataNotOkError(f"not a `{prim.__name__}`", cls=cls, x=x) + + return cast( + T, + cast(Any, x), # pyright: ignore[reportExplicitAny] + ) + + if isinstance(cls, TypedDictMeta | TypedDictMeta_te): + # TypedDict + if not isinstance(x, dict): + raise DataNotOkError("not a `dict`", cls=cls, x=x) + + spec = cast(TypedDictInstance, cast(object, cls)) + + # better error message when using the alternative syntax for closed `TypedDict`s + closed = spec.__closed__ if hasattr(spec, "__closed__") else False + extra_items = ( + spec.__extra_items__ if hasattr(spec, "__extra_items__") else NoExtraItems + ) + if extra_items is Never: + closed = True + extra_items = NoExtraItems + + missing_fields = {k for k in spec.__required_keys__ if k not in x} + extraneous_fields: set[str] = set() + if closed: + extraneous_fields = { + k + for k in x + if k not in spec.__required_keys__ and k not in spec.__optional_keys__ + } + + if len(missing_fields) > 0: + if len(extraneous_fields) > 0: + raise DataNotOkError( + f"has missing fields: {', '.join(f'{_render_field_name(f)}: {_render_type(spec.__annotations__[f])}' for f in missing_fields)}\nhas extra fields: {', '.join(_render_field_name(f) for f in extraneous_fields)}", + cls=cls, + x=x, + ) + raise DataNotOkError( + f"has missing fields: {', '.join(f'{_render_field_name(f)}: {_render_type(spec.__annotations__[f])}' for f in missing_fields)}", + cls=cls, + x=x, + ) + + if len(extraneous_fields) > 0: + raise DataNotOkError( + f"has extra fields: {', '.join(_render_field_name(f) for f in extraneous_fields)}", + cls=cls, + x=x, + ) + + for k, v in x.items(): + typ = extra_items + if k in spec.__annotations__: + typ = spec.__annotations__[k] + + if typ is NoExtraItems: + continue + + # todo(maximsmol): if the item changed (e.g. because of dataclass from dict), rebuilt the container + # todo(maximsmol): attach error context + _ = ok_data(typ, v, type_variables=type_variables) + + return cast(T, x) + + if not issubclass(cls, Generic): + if not isinstance(x, cls): + raise DataNotOkError(f"not a `{cls!r}`", cls=cls, x=x) + + return cast(T, x) + + raise FatalDataNotOkError("unsupported type", cls=cls, x=x) diff --git a/latch_data_validation/py.typed b/src/latch_data_validation/py.typed similarity index 100% rename from latch_data_validation/py.typed rename to src/latch_data_validation/py.typed diff --git a/src/tests/.DS_Store b/src/tests/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/src/tests/.DS_Store differ diff --git a/src/tests/__init__.py b/src/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/json_type.py b/src/tests/json_type.py new file mode 100644 index 0000000..1be9373 --- /dev/null +++ b/src/tests/json_type.py @@ -0,0 +1,43 @@ +from typing import ( # noqa: UP035 + Dict, # pyright: ignore[reportDeprecated] + List, # pyright: ignore[reportDeprecated] +) + +JsonValueLegacy = ( + # supported primitive values + int + | float + | str + | bool + | None # noqa: RUF036 + # > + | List["JsonValue"] # noqa: UP006 # pyright: ignore[reportDeprecated] + | Dict[str, "JsonValue"] # noqa: UP006 # pyright: ignore[reportDeprecated] +) + +JsonValueUnsupported = ( + # see https://bugs.python.org/issue41370 + # we cannot patch `list` or `types.GenericAlias` since they are immutable + # + # supported primitive values + int + | float + | str + | bool + | None # noqa: RUF036 + # > + | list["JsonValue"] + | dict[str, "JsonValue"] +) + +type JsonValue = ( + # supported primitive values + int + | float + | str + | bool + | None # noqa: RUF036 + # > + | list[JsonValue] + | dict[str, JsonValue] +) diff --git a/src/tests/test.py b/src/tests/test.py new file mode 100644 index 0000000..6ad746e --- /dev/null +++ b/src/tests/test.py @@ -0,0 +1,559 @@ +import typing +from typing import ( + Annotated, + Generic, + Literal, + NewType, + NotRequired, + ReadOnly, + Required, + TypeAlias, + TypedDict, + TypeVar, +) + +import hypothesis.strategies as st +from hypothesis import given +from typing_extensions import TypedDict as TypedDict_te # noqa: UP035 +from typing_extensions import TypeForm + +from latch_data_validation.ok_data import DataNotOkError, ok_data +from tests.json_type import JsonValue, JsonValueLegacy, JsonValueUnsupported + +type TestAlias = int +TestAliasLegacy: TypeAlias = int # noqa: UP040 +TestNewType = NewType("TestNewType", int) + + +def test_smoketest() -> None: + _ = ok_data(int, 1) + _ = ok_data(float, 1.23456789) + _ = ok_data(complex, complex(1.23456789, 9.87654321)) + _ = ok_data(bool, True) # noqa:FBT003 + _ = ok_data(str, "test") + _ = ok_data(bytes, b"test") + _ = ok_data(memoryview, memoryview(b"test")) + _ = ok_data(type(None), None) + _ = ok_data(type(...), ...) + _ = ok_data(type(NotImplemented), NotImplemented) + _ = ok_data(bytearray, bytearray(b"test")) + + _ = ok_data(Literal["a"], "a") + _ = ok_data(Literal[123, "b", True], "b") + + _ = ok_data(int | str, 123) + _ = ok_data(int | str, "test") + + try: + _ = ok_data(str, 123) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `str`" + + try: + _ = ok_data(int, True) # noqa:FBT003 + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `int`" + + try: + _ = ok_data(int | str, True) # noqa:FBT003 + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "did not match any union members" + + # >>> Special forms + _ = ok_data(TestAlias, 123) + _ = ok_data(TestAliasLegacy, 123) + _ = ok_data(Annotated[int, "test"], 123) + _ = ok_data(TestNewType, 123) + + try: + _ = ok_data(TestNewType, "test") + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `int`" + + # >>> JSON + _ = ok_data(JsonValueLegacy, {"test": [1, 1.0, False, "hi", None]}) + _ = ok_data(JsonValue, {"test": [1, 1.0, False, "hi", None]}) + + try: + _ = ok_data(JsonValueUnsupported, {"test": [1, 1.0, False, "hi", None]}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert ( + e.msg + == "using built-in `dict` (PEP585) with a string (forward reference) is not supported. Make an alias using the `type` syntax from 3.12 (PEP695)" + ) + + # >>> Lists + + _ = ok_data(list[int], [1, 2, 3]) + _ = ok_data(typing.List[int], [1, 2, 3]) # noqa: UP006 # pyright: ignore[reportDeprecated] + + try: + _ = ok_data(list[int], [1, "test", 3]) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `int`" + + try: + _ = ok_data(list[int], "not a list") + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `list`" + + try: + _ = ok_data(list["int"], [1, 2, 3]) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert ( + e.msg + == "using built-in `list` (PEP585) with a string (forward reference) is not supported. Make an alias using the `type` syntax from 3.12 (PEP695)" + ) + + # >>> Dictionaries + + _ = ok_data(dict[str, int], {"a": 1, "b": 2}) + _ = ok_data(typing.Dict[str, int], {"a": 1, "b": 2}) # noqa: UP006 # pyright: ignore[reportDeprecated] + + try: + _ = ok_data(dict[str, int], {"a": 1, 4: 2}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `str`" + try: + _ = ok_data(dict[str, int], {"a": 1, "b": "not a int"}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `int`" + + try: + _ = ok_data(dict["str", "int"], {"a": 1, "b": 2}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert ( + e.msg + == "using built-in `dict` (PEP585) with a string (forward reference) is not supported. Make an alias using the `type` syntax from 3.12 (PEP695)" + ) + + # >>> Sets + + _ = ok_data(set[int], {1, 2, 3}) + _ = ok_data(typing.Set[int], {1, 2, 3}) # noqa: UP006 # pyright: ignore[reportDeprecated] + + try: + _ = ok_data(set[int], {1, "test", 3}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `int`" + + try: + _ = ok_data(set[int], "not a set") + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `set`" + + try: + _ = ok_data(set["int"], {1, 2, 3}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert ( + e.msg + == "using built-in `set` (PEP585) with a string (forward reference) is not supported. Make an alias using the `type` syntax from 3.12 (PEP695)" + ) + + # >>> Frozen sets + + _ = ok_data(frozenset[int], frozenset({1, 2, 3})) + _ = ok_data(typing.FrozenSet[int], frozenset({1, 2, 3})) # noqa: UP006 # pyright: ignore[reportDeprecated] + + try: + _ = ok_data(frozenset[int], frozenset({1, "test", 3})) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `int`" + + try: + _ = ok_data(frozenset[int], "not a frozenset") + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `frozenset`" + + try: + _ = ok_data(frozenset[int], {1, 2, 3}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `frozenset`" + + try: + _ = ok_data(frozenset["int"], frozenset({1, 2, 3})) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert ( + e.msg + == "using built-in `frozenset` (PEP585) with a string (forward reference) is not supported. Make an alias using the `type` syntax from 3.12 (PEP695)" + ) + + # >>> Tuples + + _ = ok_data(tuple[int, str, bool], (123, "test", True)) + _ = ok_data(typing.Tuple[int, str, bool], (123, "test", True)) # noqa: UP006 # pyright: ignore[reportDeprecated] + + try: + _ = ok_data(tuple[int, str, bool], (123, "test", "not a bool")) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `bool`" + + try: + _ = ok_data(tuple[int, str, bool], "test") + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `tuple`" + + try: + _ = ok_data(tuple[int, str, bool], (123, "test")) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "incorrect tuple length: 2 (expected 3)" + + # >>> TypedDict + + _ = ok_data(TypedDict("Test", {}), {}) + _ = ok_data(TypedDict("Test", {}), {"a": 123}) + + class TestTypedDict(TypedDict): + a: int + b: Required[str] + c: ReadOnly[bool] + d: NotRequired[float] + e: int + + _ = ok_data(TestTypedDict, {"a": 123, "b": "test", "c": True, "e": 456}) + _ = ok_data( + TestTypedDict, {"a": 123, "b": "test", "c": True, "d": 1.23456789, "e": 456} + ) + + try: + _ = ok_data( + TestTypedDict, + { + # "a": 123, + "b": "test", + "c": True, + "d": 1.23456789, + "e": 456, + }, + ) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "has missing fields: a: int" + + _ = ok_data( + TestTypedDict, {"a": 123, "b": "test", "c": True, "e": 456, "f": "extra field"} + ) + + class TestTypedDictInheritance(TestTypedDict): + zzz: str + + val_typed_dict_inheritance = { + "a": 123, + "b": "test", + "c": True, + "e": 456, + "zzz": "test", + } + _ = ok_data(TestTypedDict, val_typed_dict_inheritance) + _ = ok_data(TestTypedDictInheritance, val_typed_dict_inheritance) + + class TestTypedDictNonTotal(TypedDict, total=False): + a: int + b: Required[str] + + _ = ok_data(TestTypedDictNonTotal, {"a": 123, "b": "test"}) + _ = ok_data(TestTypedDictNonTotal, {"b": "test"}) + + try: + _ = ok_data( + TestTypedDictNonTotal, + { + "a": 123 + # "b": 456 + }, + ) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "has missing fields: b: str" + + class TestTypedDictGeneric[T](TypedDict): + a: T + + _ = ok_data(TestTypedDictGeneric[int], {"a": 123}) + try: + _ = ok_data(TestTypedDictGeneric[int], {"a": "hello"}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `int`" + + T = TypeVar("T") + + class TestTypedDictGenericLegacy(TypedDict, Generic[T]): + a: T + + _ = ok_data(TestTypedDictGenericLegacy[int], {"a": 123}) + try: + _ = ok_data(TestTypedDictGenericLegacy[int], {"a": "hello"}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `int`" + + # Deprecated forms + _ = ok_data(TypedDict("Test"), {}) # pyright: ignore[reportCallIssue] + _ = ok_data(TypedDict("Test"), {"a": 123}) # pyright: ignore[reportCallIssue] + _ = ok_data(TypedDict("Test", None), {}) # pyright: ignore[reportArgumentType] + _ = ok_data(TypedDict("Test", None), {"a": 123}) # pyright: ignore[reportArgumentType] + + # PEP728 + + class TestTypedDictClosed(TypedDict_te, closed=True): + a: int + + try: + _ = ok_data(TestTypedDictClosed, {"a": 123, "b": 456}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "has extra fields: b" + + class TestTypedDictExtraItems(TypedDict_te, extra_items=str): + a: int + + _ = ok_data(TestTypedDictExtraItems, {"a": 123}) + _ = ok_data(TestTypedDictExtraItems, {"a": 123, "b": "test"}) + try: + _ = ok_data(TestTypedDictExtraItems, {"a": 123, "b": 456}) + raise AssertionError("expected exception") + except DataNotOkError as e: + assert e.msg == "not a `str`" + + class TestTypedDictExtendedGeneric(TypedDict_te, Generic[T]): + a: T + + _ = ok_data(TestTypedDictExtendedGeneric[int], {"a": 123}) + + +test_smoketest() + +# todo(maximsmol): test legacy types + +# https://docs.python.org/3/library/stdtypes.html +# todo(maximsmol): https://docs.python.org/3/reference/datamodel.html#types +st_type_basic_hashable = ( + st.just((int, st.integers())) + | st.just((float, st.floats())) + | st.just((complex, st.complex_numbers())) + | st.just((bool, st.booleans())) + # iterators? + # tuple + # range + | st.just((str, st.text())) + | st.just((bytes, st.binary())) + | st.just((memoryview, st.binary().map(memoryview))) + # context managers? + # type annotations? + # modules? + # classes? + # functions? + # methods? + # code objects? + # type objects? + | st.just((type(None), st.none())) + | st.just((type(...), st.just(...))) + | st.just((type(NotImplemented), st.just(NotImplemented))) + # frame objects? + # traceback objects? + # slice objects? +) + +st_type_basic = st_type_basic_hashable | st.just(( + bytearray, + st.binary().map(bytearray), +)) + + +def st_list[T]( + x: tuple[type[T], st.SearchStrategy[T]], +) -> tuple[type[list[T]], st.SearchStrategy[list[T]]]: + cls, gen = x + return list[cls], st.lists(gen) + + +def st_set[T]( + x: tuple[type[T], st.SearchStrategy[T]], +) -> tuple[type[set[T]], st.SearchStrategy[set[T]]]: + cls, gen = x + return set[cls], st.sets(gen) + + +def st_frozenset[T]( + x: tuple[type[T], st.SearchStrategy[T]], +) -> tuple[type[frozenset[T]], st.SearchStrategy[frozenset[T]]]: + cls, gen = x + return frozenset[cls], st.frozensets(gen) + + +def st_dict[K, V]( + k: tuple[type[K], st.SearchStrategy[K]], v: tuple[type[V], st.SearchStrategy[V]] +) -> tuple[type[dict[K, V]], st.SearchStrategy[dict[K, V]]]: + cls_k, gen_k = k + cls_v, gen_v = v + return dict[cls_k, cls_v], st.dictionaries(gen_k, gen_v) + + +# https://peps.python.org/pep-0586/ +@st.composite +def st_literal( + draw: st.DrawFn, +) -> tuple[ + Literal[object], # pyright: ignore[reportInvalidTypeForm] + st.SearchStrategy[object], +]: + x = draw(st.none() | st.integers() | st.booleans() | st.text() | st.binary()) + return Literal[x], st.just(x) + + +@st.composite +def st_type_val[T]( + draw: st.DrawFn, + base_st: st.SearchStrategy[tuple[TypeForm[object], st.SearchStrategy[object]]], +) -> tuple[TypeForm[object], object]: + cls, gen = draw(base_st) + res = draw(gen) + # print(cls, repr(res)) + return cls, res + + +# https://docs.python.org/3/library/typing.html +# TypeAlias +# Callable +# generics +# tuples +# classes +# generators +# coroutines + +# Any +# AnyStr +# LiteralString +# Never +# NoReturn +# Self +# Union +# Optional +# Concatenate +# Literal +# ClassVar +# Final +# required +# NotRequired +# ReadOnly +# Annotated +# TypeIs +# TypeGuard +# Unpack +# Generic +# TypeVar +# TypeVarTuple +# ParamSpec +# ParamSpecArgs +# ParamSpecKwargs +# TypeAliasType +# NamedTuple +# NewType +# Protocol +# TypedDict +# SupportsAbs, SupportsBytes, SupportsComplex, SupportsFloat, SupportsIndex +# SupportsInt, SupportsRound +# IO, TextIO, BinaryIO +# dataclasses +# ForwardRef +# NoDefault +# Set +# FrozenSet +# Tuple +# typing.Type +# DefaultDict +# OrderedDict +# ChainMap +# Counter +# Deque +# Pattern +# Match +# Text +# AbstractSet +# ByteString +# Collection, Container, ItemsView, KeysView, Mapping, MappingView, MutableMapping, MutableSequence +# MutableSet, Sequence, ValuesView +# Coroutine, AsyncGenerator, AsyncIterable, AsyncIterator, Awaitable +# Iterable, Iterator, Callable, Generator, Hashable, Reversible, Sized +# ContextManager, AsyncContextManager + + +@given(st_type_val(st_type_basic)) +def test_basic(data: tuple[TypeForm[object], object]) -> None: + cls, gen = data + + _ = ok_data(cls, gen) + + +@given(st_type_val(st_literal())) +def test_literal(data: tuple[TypeForm[object], object]) -> None: + cls, gen = data + + _ = ok_data(cls, gen) + + +@given(st_type_val(st_type_basic.map(st_list))) +def test_basic_list(data: tuple[TypeForm[object], object]) -> None: + cls, gen = data + + _ = ok_data(cls, gen) + + +@given(st_type_val(st_type_basic_hashable.map(st_set))) +def test_basic_set(data: tuple[TypeForm[object], object]) -> None: + cls, gen = data + + _ = ok_data(cls, gen) + + +@given(st_type_val(st_type_basic_hashable.map(st_frozenset))) +def test_basic_frozenset(data: tuple[TypeForm[object], object]) -> None: + cls, gen = data + + _ = ok_data(cls, gen) + + +@given( + st_type_val( + st.tuples(st_type_basic_hashable, st_type_basic).map( + lambda xs: st_dict(xs[0], xs[1]) + ) + ) +) +def test_basic_dict(data: tuple[TypeForm[object], object]) -> None: + cls, gen = data + + _ = ok_data(cls, gen) + + +# todo(maximsmol): test that invalid values do not validate + +# test_basic() +# test_literal() +# test_basic_list() +# test_basic_set() +# test_basic_frozenset() +# test_basic_tuple() +# test_basic_dict() diff --git a/uv.lock b/uv.lock index 38884c3..954a6ad 100644 --- a/uv.lock +++ b/uv.lock @@ -1,145 +1,95 @@ version = 1 -requires-python = ">=3.11.0" +revision = 2 +requires-python = ">=3.13.0" [[package]] -name = "deprecated" -version = "1.2.18" +name = "attrs" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] [[package]] -name = "importlib-metadata" -version = "8.5.0" +name = "hypothesis" +version = "6.135.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "attrs" }, + { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/59/7022ef95715701cd90ac0cf04582e3507492ab200f370fd7ef12d80dda75/hypothesis-6.135.4.tar.gz", hash = "sha256:c63f6fc56840558c5c5e2441dd91fad1709da60bde756b816d4b89944e50a52f", size = 451895, upload-time = "2025-06-09T02:31:38.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, + { url = "https://files.pythonhosted.org/packages/31/d4/25b3a9f35199eb1904967ca3e6db4afd636911fa39695760b0afac84f38a/hypothesis-6.135.4-py3-none-any.whl", hash = "sha256:6a3b13ce35d43e14aaf6a6ca4cc411e5342be5d05b77977499d07cf6a61e6e71", size = 517950, upload-time = "2025-06-09T02:31:34.463Z" }, ] [[package]] name = "latch-data-validation" -version = "0.1.12" +version = "2.0.0" source = { editable = "." } dependencies = [ - { name = "opentelemetry-api" }, + { name = "ruff" }, + { name = "typing-extensions" }, ] [package.dev-dependencies] dev = [ + { name = "hypothesis" }, { name = "ruff" }, ] [package.metadata] -requires-dist = [{ name = "opentelemetry-api", specifier = ">=1.15.0" }] +requires-dist = [ + { name = "ruff", specifier = ">=0.12.7" }, + { name = "typing-extensions", specifier = ">=4.14.0" }, +] [package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.9.5" }] - -[[package]] -name = "opentelemetry-api" -version = "1.30.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "deprecated" }, - { name = "importlib-metadata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/bbbf879826b7f3c89a45252010b5796fb1f1a0d45d9dc4709db0ef9a06c8/opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240", size = 63703 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/0a/eea862fae6413d8181b23acf8e13489c90a45f17986ee9cf4eab8a0b9ad9/opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09", size = 64955 }, +dev = [ + { name = "hypothesis", specifier = ">=6.135.4" }, + { name = "ruff", specifier = ">=0.11.13" }, ] [[package]] name = "ruff" -version = "0.9.6" +version = "0.12.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/81/0bd3594fa0f690466e41bd033bdcdf86cba8288345ac77ad4afbe5ec743a/ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71", size = 5197814, upload-time = "2025-07-29T22:32:35.877Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 }, - { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 }, - { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 }, - { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 }, - { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 }, - { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 }, - { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 }, - { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 }, - { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 }, - { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 }, - { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 }, - { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 }, - { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 }, - { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 }, - { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 }, - { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 }, - { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 }, + { url = "https://files.pythonhosted.org/packages/e1/d2/6cb35e9c85e7a91e8d22ab32ae07ac39cc34a71f1009a6f9e4a2a019e602/ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303", size = 11852189, upload-time = "2025-07-29T22:31:41.281Z" }, + { url = "https://files.pythonhosted.org/packages/63/5b/a4136b9921aa84638f1a6be7fb086f8cad0fde538ba76bda3682f2599a2f/ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb", size = 12519389, upload-time = "2025-07-29T22:31:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c9/3e24a8472484269b6b1821794141f879c54645a111ded4b6f58f9ab0705f/ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3", size = 11743384, upload-time = "2025-07-29T22:31:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/26/7c/458dd25deeb3452c43eaee853c0b17a1e84169f8021a26d500ead77964fd/ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860", size = 11943759, upload-time = "2025-07-29T22:32:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8b/658798472ef260ca050e400ab96ef7e85c366c39cf3dfbef4d0a46a528b6/ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c", size = 11654028, upload-time = "2025-07-29T22:32:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/a8/86/9c2336f13b2a3326d06d39178fd3448dcc7025f82514d1b15816fe42bfe8/ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423", size = 13225209, upload-time = "2025-07-29T22:32:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/76/69/df73f65f53d6c463b19b6b312fd2391dc36425d926ec237a7ed028a90fc1/ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb", size = 14182353, upload-time = "2025-07-29T22:32:10.053Z" }, + { url = "https://files.pythonhosted.org/packages/58/1e/de6cda406d99fea84b66811c189b5ea139814b98125b052424b55d28a41c/ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd", size = 13631555, upload-time = "2025-07-29T22:32:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ae/625d46d5164a6cc9261945a5e89df24457dc8262539ace3ac36c40f0b51e/ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e", size = 12667556, upload-time = "2025-07-29T22:32:15.312Z" }, + { url = "https://files.pythonhosted.org/packages/55/bf/9cb1ea5e3066779e42ade8d0cd3d3b0582a5720a814ae1586f85014656b6/ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606", size = 12939784, upload-time = "2025-07-29T22:32:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/55/7f/7ead2663be5627c04be83754c4f3096603bf5e99ed856c7cd29618c691bd/ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8", size = 11771356, upload-time = "2025-07-29T22:32:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/a95352ea16edf78cd3a938085dccc55df692a4d8ba1b3af7accbe2c806b0/ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa", size = 11612124, upload-time = "2025-07-29T22:32:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/633b04871c669e23b8917877e812376827c06df866e1677f15abfadc95cb/ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5", size = 12479945, upload-time = "2025-07-29T22:32:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/be/34/c3ef2d7799c9778b835a76189c6f53c179d3bdebc8c65288c29032e03613/ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4", size = 12998677, upload-time = "2025-07-29T22:32:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/77/ab/aca2e756ad7b09b3d662a41773f3edcbd262872a4fc81f920dc1ffa44541/ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77", size = 11756687, upload-time = "2025-07-29T22:32:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/b4/71/26d45a5042bc71db22ddd8252ca9d01e9ca454f230e2996bb04f16d72799/ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f", size = 12912365, upload-time = "2025-07-29T22:32:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9b/0b8aa09817b63e78d94b4977f18b1fcaead3165a5ee49251c5d5c245bb2d/ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69", size = 11982083, upload-time = "2025-07-29T22:32:33.881Z" }, ] [[package]] -name = "wrapt" -version = "1.17.2" +name = "sortedcontainers" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, - { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, - { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, - { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, - { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, - { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, - { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, - { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, - { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, - { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, - { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, - { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, - { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, - { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, - { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, - { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, - { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, - { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, - { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, - { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, - { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, - { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, - { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, - { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, - { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, - { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, - { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, - { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, - { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, - { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, - { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, - { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, - { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, - { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, - { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, - { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, - { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, - { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, - { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, - { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, - { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, - { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, - { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, - { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, - { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] -name = "zipp" -version = "3.21.0" +name = "typing-extensions" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, ]