diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 030f449..ff2a7b9 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -4,11 +4,13 @@ on: pull_request: jobs: lint: - name: Lint code with black + name: Lint code with ruff runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: psf/black@stable + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v1 + with: + src: ./pydantic2ts test: name: Run unit tests needs: lint @@ -16,66 +18,84 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - python-version: ["3.8"] + python-version: ["3.9"] include: - os: ubuntu-latest - python-version: "3.6" + python-version: "3.8" - os: ubuntu-latest - python-version: "3.7" + python-version: "3.10" - os: ubuntu-latest - python-version: "3.9" + python-version: "3.11" - os: ubuntu-latest - python-version: "3.10" + python-version: "3.12" + - os: ubuntu-latest + python-version: "3.13" steps: - name: Check out repo - uses: actions/checkout@v3 - - name: Set up Node.js 16 - uses: actions/setup-node@v3 - with: - node-version: 16 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/checkout@v4 + - name: Install node + uses: actions/setup-node@v4 with: - python-version: ${{ matrix.python-version }} + node-version: 20 - name: Install json-schema-to-typescript + run: npm i -g json-schema-to-typescript + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + version: "0.5.2" + - name: Run tests against 'pydantic@latest' run: | - npm i -g json-schema-to-typescript - - name: Install python dependencies - run: | - python -m pip install -U pip wheel pytest pytest-cov coverage - python -m pip install -U . - - name: Run tests - run: | - python -m pytest --cov=pydantic2ts - - name: Generate LCOV File - if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' }} + uv python install ${{ matrix.python-version }} + uv sync --all-extras --dev + uv run pytest --cov=pydantic2ts + - name: (ubuntu 3.9) Run tests against 'pydantic==1.8.2' and generate an LCOV file for Coveralls + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9' }} run: | - coverage lcov - - name: Coveralls - if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' }} - uses: coverallsapp/github-action@master + uv add 'pydantic==1.8.2' + uv run pytest --cov=pydantic2ts --cov-append + uv run coverage lcov + - name: (ubuntu 3.9) Upload to Coveralls + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9' }} + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: coverage.lcov - deploy: - name: Deploy to PyPi - runs-on: ubuntu-latest + build: + name: Build pydantic2ts for distribution if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') needs: test + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.8 - uses: actions/setup-python@v4 + - name: Check out repo + uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v3 with: - python-version: 3.8 - - name: Install dependencies - run: | - python -m pip install -U pip wheel - - name: Build dist - run: | - python setup.py sdist bdist_wheel bdist_egg - - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.5.0 + version: "0.5.2" + - name: Install python 3.9 + run: uv python install 3.9 + - name: Build pydantic2ts + run: uv build + - name: Store the distribution + uses: actions/upload-artifact@v4 + with: + name: pydantic2ts-dist + path: dist/ + publish-to-pypi: + name: Publish pydantic2ts to PyPI + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pydantic-to-typescript + permissions: + id-token: write + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 with: - user: __token__ - password: ${{ secrets.pypi_password }} \ No newline at end of file + name: pydantic2ts-dist + path: dist/ + - name: Publish distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 67eb3af..759ea7a 100644 --- a/.gitignore +++ b/.gitignore @@ -107,6 +107,7 @@ celerybeat.pid # Environments .env .venv +.venv-v1 env/ venv/ ENV/ @@ -143,3 +144,7 @@ cython_debug/ # VS Code config .vscode + +# test outputs +output_debug.ts +schema_debug.json \ No newline at end of file diff --git a/action.yml b/action.yml index a0d5e97..1317a41 100644 --- a/action.yml +++ b/action.yml @@ -2,7 +2,6 @@ name: Pydantic to Typescript description: | Convert pydantic models into typescript definitions and ensure that your type definitions are in sync. author: Phillip Dupuis - inputs: python-module: required: true @@ -44,17 +43,17 @@ runs: using: composite steps: - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: ">=3.6 <=3.10" + python-version: "3.x" - name: Install pydantic-to-typescript shell: bash run: | python -m pip install -U pip wheel pydantic-to-typescript - - name: Set up Node.js 16 - uses: actions/setup-node@v3 + - name: Set up Node.js 20 + uses: actions/setup-node@v4 with: - node-version: 16 + node-version: 20 - name: Install json-schema-to-typescript shell: bash run: | diff --git a/pydantic2ts/__init__.py b/pydantic2ts/__init__.py index 33ac1c5..0ffe0bc 100644 --- a/pydantic2ts/__init__.py +++ b/pydantic2ts/__init__.py @@ -1 +1,3 @@ from pydantic2ts.cli.script import generate_typescript_defs + +__all__ = ("generate_typescript_defs",) diff --git a/pydantic2ts/cli/script.py b/pydantic2ts/cli/script.py index 8518395..76fc9a9 100644 --- a/pydantic2ts/cli/script.py +++ b/pydantic2ts/cli/script.py @@ -6,23 +6,39 @@ import os import shutil import sys +from contextlib import ExitStack, contextmanager from importlib.util import module_from_spec, spec_from_file_location from tempfile import mkdtemp from types import ModuleType -from typing import Any, Dict, List, Tuple, Type +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Generator, + List, + Optional, + Tuple, + Type, + Union, +) from uuid import uuid4 -from pydantic import BaseModel, Extra, create_model +import pydantic2ts.pydantic_v1 as v1 +import pydantic2ts.pydantic_v2 as v2 -try: - from pydantic.generics import GenericModel -except ImportError: - GenericModel = None +if TYPE_CHECKING: # pragma: no cover + from pydantic.config import ConfigDict + from pydantic.v1.config import BaseConfig + from pydantic.v1.fields import ModelField -logger = logging.getLogger("pydantic2ts") +LOG = logging.getLogger("pydantic2ts") -def import_module(path: str) -> ModuleType: +_USELESS_ENUM_DESCRIPTION = "An enumeration." +_USELESS_STR_DESCRIPTION = inspect.getdoc(str) + + +def _import_module(path: str) -> ModuleType: """ Helper which allows modules to be specified by either dotted path notation or by filepath. @@ -34,69 +50,163 @@ def import_module(path: str) -> ModuleType: if os.path.exists(path): name = uuid4().hex spec = spec_from_file_location(name, path, submodule_search_locations=[]) + assert spec is not None, f"spec_from_file_location failed for {path}" module = module_from_spec(spec) sys.modules[name] = module + assert spec.loader is not None, f"loader is None for {path}" spec.loader.exec_module(module) return module else: return importlib.import_module(path) except Exception as e: - logger.error( + LOG.error( "The --module argument must be a module path separated by dots or a valid filepath" ) raise e -def is_submodule(obj, module_name: str) -> bool: +def _is_submodule(obj: Any, module_name: str) -> bool: """ Return true if an object is a submodule """ - return inspect.ismodule(obj) and getattr(obj, "__name__", "").startswith( - f"{module_name}." - ) + return inspect.ismodule(obj) and getattr(obj, "__name__", "").startswith(f"{module_name}.") -def is_concrete_pydantic_model(obj) -> bool: +def _is_v1_model(obj: Any) -> bool: """ - Return true if an object is a concrete subclass of pydantic's BaseModel. - 'concrete' meaning that it's not a GenericModel. + Return true if an object is a 'concrete' pydantic V1 model. """ if not inspect.isclass(obj): return False - elif obj is BaseModel: + elif obj is v1.BaseModel: return False - elif GenericModel and issubclass(obj, GenericModel): + elif v1.GenericModel and issubclass(obj, v1.GenericModel): return bool(obj.__concrete__) else: - return issubclass(obj, BaseModel) + return issubclass(obj, v1.BaseModel) + + +def _is_v2_model(obj: Any) -> bool: + """ + Return true if an object is a 'concrete' pydantic V2 model. + """ + if not v2.enabled: + return False + elif not inspect.isclass(obj): + return False + elif obj is v2.BaseModel: + return False + elif not issubclass(obj, v2.BaseModel): + return False + generic_metadata = getattr(obj, "__pydantic_generic_metadata__", {}) + generic_parameters = generic_metadata.get("parameters") + return not generic_parameters + + +def _is_pydantic_model(obj: Any) -> bool: + """ + Return true if an object is a concrete model for either V1 or V2 of pydantic. + """ + return _is_v1_model(obj) or _is_v2_model(obj) -def extract_pydantic_models(module: ModuleType) -> List[Type[BaseModel]]: +def _is_nullable(schema: Dict[str, Any]) -> bool: + """ + Return true if a JSON schema has 'null' as one of its types. + """ + if schema.get("type") == "null": + return True + if isinstance(schema.get("type"), list) and "null" in schema["type"]: + return True + if isinstance(schema.get("anyOf"), list): + return any(_is_nullable(s) for s in schema["anyOf"]) + return False + + +def _get_model_config(model: Type[Any]) -> "Union[ConfigDict, Type[BaseConfig]]": + """ + Return the 'config' for a pydantic model. + In version 1 of pydantic, this is a class. In version 2, it's a dictionary. + """ + if hasattr(model, "Config") and inspect.isclass(model.Config): + return model.Config + return model.model_config + + +def _get_model_json_schema(model: Type[Any]) -> Dict[str, Any]: + """ + Generate the JSON schema for a pydantic model. + """ + if _is_v1_model(model): + return json.loads(model.schema_json()) + return model.model_json_schema(mode="serialization") + + +def _extract_pydantic_models(module: ModuleType) -> List[type]: """ Given a module, return a list of the pydantic models contained within it. """ - models = [] + models: List[type] = [] module_name = module.__name__ - for _, model in inspect.getmembers(module, is_concrete_pydantic_model): + for _, model in inspect.getmembers(module, _is_pydantic_model): models.append(model) - for _, submodule in inspect.getmembers( - module, lambda obj: is_submodule(obj, module_name) - ): - models.extend(extract_pydantic_models(submodule)) + for _, submodule in inspect.getmembers(module, lambda obj: _is_submodule(obj, module_name)): + models.extend(_extract_pydantic_models(submodule)) return models -def clean_output_file(output_filename: str) -> None: +def _clean_json_schema(schema: Dict[str, Any], model: Any = None) -> None: """ - Clean up the output file typescript definitions were written to by: - 1. Removing the 'master model'. - This is a faux pydantic model with references to all the *actual* models necessary for generating - clean typescript definitions without any duplicates. We don't actually want it in the output, so - this function removes it from the generated typescript file. - 2. Adding a banner comment with clear instructions for how to regenerate the typescript definitions. + Clean up the resulting JSON schemas via the following steps: + + 1) Get rid of descriptions that are auto-generated and just add noise: + - "An enumeration." for Enums + - `inspect.getdoc(str)` for Literal types + 2) Remove titles from JSON schema properties. + If we don't do this, each property will have its own interface in the + resulting typescript file (which is a LOT of unnecessary noise). + 3) If it's a V1 model, ensure that nullability is properly represented. + https://github.com/pydantic/pydantic/issues/1270 + """ + description = schema.get("description") + + if "enum" in schema and description == _USELESS_ENUM_DESCRIPTION: + del schema["description"] + elif description == _USELESS_STR_DESCRIPTION: + del schema["description"] + + properties: Dict[str, Dict[str, Any]] = schema.get("properties", {}) + + for prop in properties.values(): + prop.pop("title", None) + + if _is_v1_model(model): + fields: List["ModelField"] = list(model.__fields__.values()) + fields_that_should_be_nullable = [f for f in fields if f.allow_none] + for field in fields_that_should_be_nullable: + try: + name = field.alias + prop = properties.get(field.alias) + if prop and not _is_nullable(prop): + properties[name] = {"anyOf": [prop, {"type": "null"}]} + except Exception: + LOG.error( + f"Failed to ensure nullability for field {field.alias}.", + exc_info=True, + ) + + +def _clean_output_file(output_filename: str) -> None: + """ + Clean up the resulting typescript definitions via the following steps: + + 1. Remove the "_Master_" model. + It exists solely to serve as a namespace for the target models. + By rolling them all up into a single model, we can generate a single output file. + 2. Add a banner comment with clear instructions for regenerating the typescript definitions. """ with open(output_filename, "r") as f: lines = f.readlines() @@ -109,6 +219,9 @@ def clean_output_file(output_filename: str) -> None: end = i break + assert start is not None, "Could not find the start of the _Master_ interface." + assert end is not None, "Could not find the end of the _Master_ interface." + banner_comment_lines = [ "/* tslint:disable */\n", "/* eslint-disable */\n", @@ -124,71 +237,82 @@ def clean_output_file(output_filename: str) -> None: f.writelines(new_lines) -def clean_schema(schema: Dict[str, Any]) -> None: +@contextmanager +def _schema_generation_overrides( + model: Type[Any], +) -> Generator[None, None, None]: """ - Clean up the resulting JSON schemas by: - - 1) Removing titles from JSON schema properties. - If we don't do this, each property will have its own interface in the - resulting typescript file (which is a LOT of unnecessary noise). - 2) Getting rid of the useless "An enumeration." description applied to Enums - which don't have a docstring. + Temporarily override the 'extra' setting for a model, + changing it to 'forbid' unless it was EXPLICITLY set to 'allow'. + This prevents '[k: string]: any' from automatically being added to every interface. """ - for prop in schema.get("properties", {}).values(): - prop.pop("title", None) - - if "enum" in schema and schema.get("description") == "An enumeration.": - del schema["description"] + revert: Dict[str, Any] = {} + config = _get_model_config(model) + try: + if isinstance(config, dict): + if config.get("extra") != "allow": + revert["extra"] = config.get("extra") + config["extra"] = "forbid" + else: + if config.extra != "allow": + revert["extra"] = config.extra + config.extra = "forbid" # type: ignore + yield + finally: + for key, value in revert.items(): + if isinstance(config, dict): + config[key] = value + else: + setattr(config, key, value) -def generate_json_schema(models: List[Type[BaseModel]]) -> str: +def _generate_json_schema(models: List[type]) -> str: """ Create a top-level '_Master_' model with references to each of the actual models. Generate the schema for this model, which will include the schemas for all the nested models. Then clean up the schema. - - One weird thing we do is we temporarily override the 'extra' setting in models, - changing it to 'forbid' UNLESS it was explicitly set to 'allow'. This prevents - '[k: string]: any' from being added to every interface. This change is reverted - once the schema has been generated. """ - model_extras = [getattr(m.Config, "extra", None) for m in models] + with ExitStack() as stack: + models_by_name: Dict[str, type] = {} + models_as_fields: Dict[str, Tuple[type, Any]] = {} - try: - for m in models: - if getattr(m.Config, "extra", None) != Extra.allow: - m.Config.extra = Extra.forbid + for model in models: + stack.enter_context(_schema_generation_overrides(model)) + name = model.__name__ + models_by_name[name] = model + models_as_fields[name] = (model, ...) - master_model = create_model( - "_Master_", **{m.__name__: (m, ...) for m in models} - ) - master_model.Config.extra = Extra.forbid - master_model.Config.schema_extra = staticmethod(clean_schema) + use_v1_tools = any(issubclass(m, v1.BaseModel) for m in models) + create_model = v1.create_model if use_v1_tools else v2.create_model # type: ignore + master_model = create_model("_Master_", **models_as_fields) # type: ignore + master_schema = _get_model_json_schema(master_model) # type: ignore - schema = json.loads(master_model.schema_json()) + defs_key = "$defs" if "$defs" in master_schema else "definitions" + defs: Dict[str, Any] = master_schema.get(defs_key, {}) - for d in schema.get("definitions", {}).values(): - clean_schema(d) + for name, schema in defs.items(): + _clean_json_schema(schema, models_by_name.get(name)) - return json.dumps(schema, indent=2) - - finally: - for m, x in zip(models, model_extras): - if x is not None: - m.Config.extra = x + return json.dumps(master_schema, indent=2) def generate_typescript_defs( - module: str, output: str, exclude: Tuple[str] = (), json2ts_cmd: str = "json2ts" + module: str, + output: str, + exclude: Tuple[str, ...] = (), + json2ts_cmd: str = "json2ts", ) -> None: """ Convert the pydantic models in a python module into typescript interfaces. - :param module: python module containing pydantic model definitions, ex: my_project.api.schemas + :param module: python module containing pydantic model definitions. + example: my_project.api.schemas :param output: file that the typescript definitions will be written to - :param exclude: optional, a tuple of names for pydantic models which should be omitted from the typescript output. - :param json2ts_cmd: optional, the command that will execute json2ts. Provide this if the executable is not - discoverable or if it's locally installed (ex: 'yarn json2ts'). + :param exclude: optional, a tuple of names for pydantic models which + should be omitted from the typescript output. + :param json2ts_cmd: optional, the command that will execute json2ts. + Provide this if the executable is not discoverable + or if it's locally installed (ex: 'yarn json2ts'). """ if " " not in json2ts_cmd and not shutil.which(json2ts_cmd): raise Exception( @@ -196,23 +320,29 @@ def generate_typescript_defs( "https://www.npmjs.com/package/json-schema-to-typescript" ) - logger.info("Finding pydantic models...") + LOG.info("Finding pydantic models...") - models = extract_pydantic_models(import_module(module)) + models = _extract_pydantic_models(_import_module(module)) if exclude: - models = [m for m in models if m.__name__ not in exclude] + models = [ + m for m in models if (m.__name__ not in exclude and m.__qualname__ not in exclude) + ] + + if not models: + LOG.info("No pydantic models found, exiting.") + return - logger.info("Generating JSON schema from pydantic models...") + LOG.info("Generating JSON schema from pydantic models...") - schema = generate_json_schema(models) + schema = _generate_json_schema(models) schema_dir = mkdtemp() schema_file_path = os.path.join(schema_dir, "schema.json") with open(schema_file_path, "w") as f: f.write(schema) - logger.info("Converting JSON schema to typescript definitions...") + LOG.info("Converting JSON schema to typescript definitions...") json2ts_exit_code = os.system( f'{json2ts_cmd} -i {schema_file_path} -o {output} --bannerComment ""' @@ -221,15 +351,13 @@ def generate_typescript_defs( shutil.rmtree(schema_dir) if json2ts_exit_code == 0: - clean_output_file(output) - logger.info(f"Saved typescript definitions to {output}.") + _clean_output_file(output) + LOG.info(f"Saved typescript definitions to {output}.") else: - raise RuntimeError( - f'"{json2ts_cmd}" failed with exit code {json2ts_exit_code}.' - ) + raise RuntimeError(f'"{json2ts_cmd}" failed with exit code {json2ts_exit_code}.') -def parse_cli_args(args: List[str] = None) -> argparse.Namespace: +def parse_cli_args(args: Optional[List[str]] = None) -> argparse.Namespace: """ Parses the command-line arguments passed to pydantic2ts. """ @@ -242,10 +370,12 @@ def parse_cli_args(args: List[str] = None) -> argparse.Namespace: "--module", help="name or filepath of the python module.\n" "Discoverable submodules will also be checked.", + required=True, ) parser.add_argument( "--output", help="name of the file the typescript definitions should be written to.", + required=True, ) parser.add_argument( "--exclude", diff --git a/pydantic2ts/pydantic_v1.py b/pydantic2ts/pydantic_v1.py new file mode 100644 index 0000000..88016cd --- /dev/null +++ b/pydantic2ts/pydantic_v1.py @@ -0,0 +1,16 @@ +try: + from pydantic.v1 import BaseModel, create_model # type: ignore + from pydantic.v1.generics import GenericModel + + enabled = True +except ImportError: + from pydantic import BaseModel, create_model + + enabled = True + + try: + from pydantic.generics import GenericModel + except ImportError: # pragma: no cover + GenericModel = None + +__all__ = ("BaseModel", "GenericModel", "create_model", "enabled") diff --git a/pydantic2ts/pydantic_v2.py b/pydantic2ts/pydantic_v2.py new file mode 100644 index 0000000..5633bd7 --- /dev/null +++ b/pydantic2ts/pydantic_v2.py @@ -0,0 +1,14 @@ +try: + from pydantic.version import VERSION + + assert VERSION.startswith("2") + + from pydantic import BaseModel, create_model + + enabled = True +except (ImportError, AssertionError, AttributeError): + BaseModel = None + create_model = None + enabled = False + +__all__ = ("BaseModel", "create_model", "enabled") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d08b4eb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[project] +name = "pydantic-to-typescript" +version = "2.0.0" +description = "Convert pydantic models to typescript interfaces" +authors = [ + {name = "Phillip Dupuis", email = "phillip_dupuis@alumni.brown.edu"}, +] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.8" +keywords = ["pydantic", "typescript", "annotations", "validation", "interface"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "pydantic", +] + +[project.urls] +Homepage = "https://github.com/phillipdupuis/pydantic-to-typescript" +Repository = "https://github.com/phillipdupuis/pydantic-to-typescript" + +[project.scripts] +pydantic2ts = "pydantic2ts.cli.script:main" + +[project.optional-dependencies] +dev = [ + "pytest", + "pytest-cov", + "coverage", + "ruff", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["pydantic2ts"] + +[tool.ruff] +line-length = 100 +indent-width = 4 + +[tool.ruff.format] +quote-style = "double" + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "W"] +fixable = ["ALL"] +ignore = ["E501"] \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index dff85f4..0000000 --- a/setup.py +++ /dev/null @@ -1,44 +0,0 @@ -from setuptools import setup, find_packages - - -def readme(): - with open("README.md", "r") as infile: - return infile.read() - - -classifiers = [ - "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", -] - -install_requires = [ - "pydantic", -] - -setup( - name="pydantic-to-typescript", - version="1.0.10", - description="Convert pydantic models to typescript interfaces", - license="MIT", - long_description=readme(), - long_description_content_type="text/markdown", - keywords="pydantic typescript annotations validation interface", - author="Phillip Dupuis", - author_email="phillip_dupuis@alumni.brown.edu", - url="https://github.com/phillipdupuis/pydantic-to-typescript", - packages=find_packages(exclude=["tests*"]), - install_requires=install_requires, - extras_require={ - "dev": ["pytest", "pytest-cov", "coverage"], - }, - entry_points={"console_scripts": ["pydantic2ts = pydantic2ts.cli.script:main"]}, - classifiers=classifiers, -) diff --git a/tests/expected_results/computed_fields/output.ts b/tests/expected_results/computed_fields/output.ts new file mode 100644 index 0000000..b371eac --- /dev/null +++ b/tests/expected_results/computed_fields/output.ts @@ -0,0 +1,12 @@ +/* tslint:disable */ +/* eslint-disable */ +/** +/* This file was automatically generated from pydantic models by running pydantic2ts. +/* Do not modify it by hand - just update the pydantic models and then re-run the script +*/ + +export interface Rectangle { + width: number; + length: number; + area: number; +} diff --git a/tests/expected_results/computed_fields/v2_input.py b/tests/expected_results/computed_fields/v2_input.py new file mode 100644 index 0000000..c05f9b1 --- /dev/null +++ b/tests/expected_results/computed_fields/v2_input.py @@ -0,0 +1,13 @@ +# https://docs.pydantic.dev/latest/usage/computed_fields/ + +from pydantic import BaseModel, computed_field + + +class Rectangle(BaseModel): + width: int + length: int + + @computed_field + @property + def area(self) -> int: + return self.width * self.length diff --git a/tests/expected_results/excluding_models/output.ts b/tests/expected_results/excluding_models/output.ts index 451bb73..24a09cf 100644 --- a/tests/expected_results/excluding_models/output.ts +++ b/tests/expected_results/excluding_models/output.ts @@ -7,6 +7,6 @@ export interface Profile { username: string; - age?: number; + age?: number | null; hobbies: string[]; } diff --git a/tests/expected_results/excluding_models/v1_input.py b/tests/expected_results/excluding_models/v1_input.py new file mode 100644 index 0000000..09de964 --- /dev/null +++ b/tests/expected_results/excluding_models/v1_input.py @@ -0,0 +1,22 @@ +from typing import List, Optional + +try: + from pydantic.v1 import BaseModel +except ImportError: + from pydantic import BaseModel + + +class LoginCredentials(BaseModel): + username: str + password: str + + +class Profile(BaseModel): + username: str + age: Optional[int] = None + hobbies: List[str] + + +class LoginResponseData(BaseModel): + token: str + profile: Profile diff --git a/tests/expected_results/excluding_models/input.py b/tests/expected_results/excluding_models/v2_input.py similarity index 79% rename from tests/expected_results/excluding_models/input.py rename to tests/expected_results/excluding_models/v2_input.py index e37ee05..b3ee887 100644 --- a/tests/expected_results/excluding_models/input.py +++ b/tests/expected_results/excluding_models/v2_input.py @@ -1,5 +1,6 @@ +from typing import List, Optional + from pydantic import BaseModel -from typing import Optional, List class LoginCredentials(BaseModel): @@ -9,7 +10,7 @@ class LoginCredentials(BaseModel): class Profile(BaseModel): username: str - age: Optional[int] + age: Optional[int] = None hobbies: List[str] diff --git a/tests/expected_results/extra_fields/output.ts b/tests/expected_results/extra_fields/output.ts new file mode 100644 index 0000000..eba327c --- /dev/null +++ b/tests/expected_results/extra_fields/output.ts @@ -0,0 +1,20 @@ +/* tslint:disable */ +/* eslint-disable */ +/** +/* This file was automatically generated from pydantic models by running pydantic2ts. +/* Do not modify it by hand - just update the pydantic models and then re-run the script +*/ + +export interface ModelExtraAllow { + a: string; + [k: string]: unknown; +} +export interface ModelExtraForbid { + a: string; +} +export interface ModelExtraIgnore { + a: string; +} +export interface ModelExtraNone { + a: string; +} diff --git a/tests/expected_results/extra_fields/v1_input.py b/tests/expected_results/extra_fields/v1_input.py new file mode 100644 index 0000000..6395315 --- /dev/null +++ b/tests/expected_results/extra_fields/v1_input.py @@ -0,0 +1,29 @@ +try: + from pydantic.v1 import BaseConfig, BaseModel, Extra +except ImportError: + from pydantic import BaseConfig, BaseModel, Extra + + +class ModelExtraAllow(BaseModel): + a: str + + class Config(BaseConfig): + extra = Extra.allow + + +class ModelExtraForbid(BaseModel): + a: str + + class Config(BaseConfig): + extra = Extra.forbid + + +class ModelExtraIgnore(BaseModel): + a: str + + class Config(BaseConfig): + extra = Extra.ignore + + +class ModelExtraNone(BaseModel): + a: str diff --git a/tests/expected_results/extra_fields/v2_input.py b/tests/expected_results/extra_fields/v2_input.py new file mode 100644 index 0000000..f6010ad --- /dev/null +++ b/tests/expected_results/extra_fields/v2_input.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel, ConfigDict + + +class ModelExtraAllow(BaseModel): + model_config = ConfigDict(extra="allow") + a: str + + +class ModelExtraForbid(BaseModel): + model_config = ConfigDict(extra="forbid") + a: str + + +class ModelExtraIgnore(BaseModel): + model_config = ConfigDict(extra="ignore") + a: str + + +class ModelExtraNone(BaseModel): + a: str diff --git a/tests/expected_results/generics/output.ts b/tests/expected_results/generics/output.ts index 5da2624..eafac05 100644 --- a/tests/expected_results/generics/output.ts +++ b/tests/expected_results/generics/output.ts @@ -19,12 +19,12 @@ export interface Error { message: string; } export interface ListArticlesResponse { - data?: Article[]; - error?: Error; + data?: Article[] | null; + error?: Error | null; } export interface ListUsersResponse { - data?: User[]; - error?: Error; + data?: User[] | null; + error?: Error | null; } export interface UserProfile { name: string; @@ -34,6 +34,6 @@ export interface UserProfile { age: number; } export interface UserProfileResponse { - data?: UserProfile; - error?: Error; + data?: UserProfile | null; + error?: Error | null; } diff --git a/tests/expected_results/generics/v1_input.py b/tests/expected_results/generics/v1_input.py new file mode 100644 index 0000000..6790d80 --- /dev/null +++ b/tests/expected_results/generics/v1_input.py @@ -0,0 +1,57 @@ +from datetime import datetime +from typing import Generic, List, Optional, Type, TypeVar + +try: + from pydantic.v1 import BaseModel + from pydantic.v1.generics import GenericModel +except ImportError: + from pydantic import BaseModel + from pydantic.generics import GenericModel + +T = TypeVar("T") + + +class Error(BaseModel): + code: int + message: str + + +class ApiResponse(GenericModel, Generic[T]): + data: Optional[T] = None + error: Optional[Error] = None + + +def create_response_type(data_type: T, name: str) -> "Type[ApiResponse[T]]": + """ + Create a concrete implementation of ApiResponse and then applies the specified name. + This is necessary because the name automatically generated by __concrete_name__ is + really ugly, it just doesn't look good. + """ + t = ApiResponse[data_type] + t.__name__ = name + t.__qualname__ = name + return t + + +class User(BaseModel): + name: str + email: str + + +class UserProfile(User): + joined: datetime + last_active: datetime + age: int + + +class Article(BaseModel): + author: User + content: str + published: datetime + + +ListUsersResponse = create_response_type(List[User], "ListUsersResponse") + +ListArticlesResponse = create_response_type(List[Article], "ListArticlesResponse") + +UserProfileResponse = create_response_type(UserProfile, "UserProfileResponse") diff --git a/tests/expected_results/generics/input.py b/tests/expected_results/generics/v2_input.py similarity index 80% rename from tests/expected_results/generics/input.py rename to tests/expected_results/generics/v2_input.py index a37bc55..8a71733 100644 --- a/tests/expected_results/generics/input.py +++ b/tests/expected_results/generics/v2_input.py @@ -1,8 +1,7 @@ from datetime import datetime -from typing import Generic, TypeVar, Optional, List, Type, cast, Union +from typing import Generic, List, Optional, Type, TypeVar from pydantic import BaseModel -from pydantic.generics import GenericModel T = TypeVar("T") @@ -12,9 +11,9 @@ class Error(BaseModel): message: str -class ApiResponse(GenericModel, Generic[T]): - data: Optional[T] - error: Optional[Error] +class ApiResponse(BaseModel, Generic[T]): + data: Optional[T] = None + error: Optional[Error] = None def create_response_type(data_type: T, name: str) -> "Type[ApiResponse[T]]": @@ -26,7 +25,7 @@ def create_response_type(data_type: T, name: str) -> "Type[ApiResponse[T]]": t = ApiResponse[data_type] t.__name__ = name t.__qualname__ = name - return cast(Type[ApiResponse[T]], t) + return t class User(BaseModel): diff --git a/tests/expected_results/single_module/output.ts b/tests/expected_results/single_module/output.ts index 4defc90..05bf779 100644 --- a/tests/expected_results/single_module/output.ts +++ b/tests/expected_results/single_module/output.ts @@ -15,6 +15,6 @@ export interface LoginResponseData { } export interface Profile { username: string; - age?: number; + age?: number | null; hobbies: string[]; } diff --git a/tests/expected_results/single_module/v1_input.py b/tests/expected_results/single_module/v1_input.py new file mode 100644 index 0000000..09de964 --- /dev/null +++ b/tests/expected_results/single_module/v1_input.py @@ -0,0 +1,22 @@ +from typing import List, Optional + +try: + from pydantic.v1 import BaseModel +except ImportError: + from pydantic import BaseModel + + +class LoginCredentials(BaseModel): + username: str + password: str + + +class Profile(BaseModel): + username: str + age: Optional[int] = None + hobbies: List[str] + + +class LoginResponseData(BaseModel): + token: str + profile: Profile diff --git a/tests/expected_results/single_module/input.py b/tests/expected_results/single_module/v2_input.py similarity index 79% rename from tests/expected_results/single_module/input.py rename to tests/expected_results/single_module/v2_input.py index e37ee05..b3ee887 100644 --- a/tests/expected_results/single_module/input.py +++ b/tests/expected_results/single_module/v2_input.py @@ -1,5 +1,6 @@ +from typing import List, Optional + from pydantic import BaseModel -from typing import Optional, List class LoginCredentials(BaseModel): @@ -9,7 +10,7 @@ class LoginCredentials(BaseModel): class Profile(BaseModel): username: str - age: Optional[int] + age: Optional[int] = None hobbies: List[str] diff --git a/tests/expected_results/submodules/animals/__init__.py b/tests/expected_results/submodules/v1_animals/__init__.py similarity index 100% rename from tests/expected_results/submodules/animals/__init__.py rename to tests/expected_results/submodules/v1_animals/__init__.py diff --git a/tests/expected_results/submodules/v1_animals/cats.py b/tests/expected_results/submodules/v1_animals/cats.py new file mode 100644 index 0000000..9b8b803 --- /dev/null +++ b/tests/expected_results/submodules/v1_animals/cats.py @@ -0,0 +1,20 @@ +from enum import Enum + +try: + from pydantic.v1 import BaseModel +except ImportError: + from pydantic import BaseModel + + +class CatBreed(str, Enum): + domestic_shorthair = "domestic shorthair" + bengal = "bengal" + persian = "persian" + siamese = "siamese" + + +class Cat(BaseModel): + name: str + age: int + declawed: bool + breed: CatBreed diff --git a/tests/expected_results/submodules/v1_animals/dogs.py b/tests/expected_results/submodules/v1_animals/dogs.py new file mode 100644 index 0000000..754b449 --- /dev/null +++ b/tests/expected_results/submodules/v1_animals/dogs.py @@ -0,0 +1,18 @@ +from enum import Enum + +try: + from pydantic.v1 import BaseModel +except ImportError: + from pydantic import BaseModel + + +class DogBreed(str, Enum): + mutt = "mutt" + labrador = "labrador" + golden_retriever = "golden retriever" + + +class Dog(BaseModel): + name: str + age: int + breed: DogBreed diff --git a/tests/expected_results/submodules/v1_input.py b/tests/expected_results/submodules/v1_input.py new file mode 100644 index 0000000..faf40fb --- /dev/null +++ b/tests/expected_results/submodules/v1_input.py @@ -0,0 +1,15 @@ +from typing import List + +try: + from pydantic.v1 import BaseModel +except ImportError: + from pydantic import BaseModel + +from .v1_animals.cats import Cat +from .v1_animals.dogs import Dog + + +class AnimalShelter(BaseModel): + address: str + cats: List[Cat] + dogs: List[Dog] diff --git a/tests/expected_results/submodules/v2_animals/__init__.py b/tests/expected_results/submodules/v2_animals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/expected_results/submodules/animals/cats.py b/tests/expected_results/submodules/v2_animals/cats.py similarity index 88% rename from tests/expected_results/submodules/animals/cats.py rename to tests/expected_results/submodules/v2_animals/cats.py index 3db89d3..542db86 100644 --- a/tests/expected_results/submodules/animals/cats.py +++ b/tests/expected_results/submodules/v2_animals/cats.py @@ -1,7 +1,7 @@ -from pydantic import BaseModel -from typing import Optional, Literal from enum import Enum +from pydantic import BaseModel + class CatBreed(str, Enum): domestic_shorthair = "domestic shorthair" diff --git a/tests/expected_results/submodules/animals/dogs.py b/tests/expected_results/submodules/v2_animals/dogs.py similarity index 89% rename from tests/expected_results/submodules/animals/dogs.py rename to tests/expected_results/submodules/v2_animals/dogs.py index 07ec007..8bb4f46 100644 --- a/tests/expected_results/submodules/animals/dogs.py +++ b/tests/expected_results/submodules/v2_animals/dogs.py @@ -1,7 +1,7 @@ -from pydantic import BaseModel -from typing import Optional from enum import Enum +from pydantic import BaseModel + class DogBreed(str, Enum): mutt = "mutt" diff --git a/tests/expected_results/submodules/input.py b/tests/expected_results/submodules/v2_input.py similarity index 68% rename from tests/expected_results/submodules/input.py rename to tests/expected_results/submodules/v2_input.py index c769f5c..2cccb4c 100644 --- a/tests/expected_results/submodules/input.py +++ b/tests/expected_results/submodules/v2_input.py @@ -1,7 +1,9 @@ -from pydantic import BaseModel from typing import List -from .animals.cats import Cat -from .animals.dogs import Dog + +from pydantic import BaseModel + +from .v2_animals.cats import Cat +from .v2_animals.dogs import Dog class AnimalShelter(BaseModel): diff --git a/tests/test_script.py b/tests/test_script.py index 8ff3e1f..ea2db6d 100644 --- a/tests/test_script.py +++ b/tests/test_script.py @@ -1,36 +1,44 @@ import os import subprocess -import sys +from itertools import product +from pathlib import Path +from typing import Optional, Tuple import pytest from pydantic2ts import generate_typescript_defs from pydantic2ts.cli.script import parse_cli_args +from pydantic2ts.pydantic_v2 import enabled as v2_enabled - -def _results_directory() -> str: - return os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected_results") +_PYDANTIC_VERSIONS = (1, 2) if v2_enabled else (1,) +_RESULTS_DIRECTORY = Path( + os.path.join(os.path.dirname(os.path.realpath(__file__)), "expected_results") +) -def get_input_module(test_name: str) -> str: - return os.path.join(_results_directory(), test_name, "input.py") +def _python_module_path(test_name: str, pydantic_version: int) -> str: + return str(_RESULTS_DIRECTORY / test_name / f"v{pydantic_version}_input.py") -def get_expected_output(test_name: str) -> str: - path = os.path.join(_results_directory(), test_name, "output.ts") - with open(path, "r") as f: - return f.read() +def _expected_typescript_code(test_name: str) -> str: + return (_RESULTS_DIRECTORY / test_name / "output.ts").read_text() -def run_test( - tmpdir, test_name, *, module_path=None, call_from_python=False, exclude=() +def _run_test( + tmp_path: Path, + test_name: str, + pydantic_version: int, + *, + module_path: Optional[str] = None, + call_from_python: bool = False, + exclude: Tuple[str, ...] = (), ): """ Execute pydantic2ts logic for converting pydantic models into tyepscript definitions. Compare the output with the expected output, verifying it is identical. """ - module_path = module_path or get_input_module(test_name) - output_path = tmpdir.join(f"cli_{test_name}.ts").strpath + module_path = module_path or _python_module_path(test_name, pydantic_version) + output_path = str(tmp_path / f"{test_name}_v{pydantic_version}.ts") if call_from_python: generate_typescript_defs(module_path, output_path, exclude) @@ -38,71 +46,81 @@ def run_test( cmd = f"pydantic2ts --module {module_path} --output {output_path}" for model_to_exclude in exclude: cmd += f" --exclude {model_to_exclude}" - subprocess.run(cmd, shell=True) + subprocess.run(cmd, shell=True, check=True) - with open(output_path, "r") as f: - output = f.read() - assert output == get_expected_output(test_name) + assert Path(output_path).read_text() == _expected_typescript_code(test_name) -def test_single_module(tmpdir): - run_test(tmpdir, "single_module") +@pytest.mark.parametrize( + "pydantic_version, call_from_python", + product(_PYDANTIC_VERSIONS, [False, True]), +) +def test_single_module(tmp_path: Path, pydantic_version: int, call_from_python: bool): + _run_test(tmp_path, "single_module", pydantic_version, call_from_python=call_from_python) -@pytest.mark.skipif( - sys.version_info < (3, 8), - reason="Literal requires python 3.8 or higher (Ref.: PEP 586)", +@pytest.mark.parametrize( + "pydantic_version, call_from_python", + product(_PYDANTIC_VERSIONS, [False, True]), ) -def test_submodules(tmpdir): - run_test(tmpdir, "submodules") +def test_submodules(tmp_path: Path, pydantic_version: int, call_from_python: bool): + _run_test(tmp_path, "submodules", pydantic_version, call_from_python=call_from_python) -@pytest.mark.skipif( - sys.version_info < (3, 7), - reason=( - "GenericModel is only supported for python>=3.7 " - "(Ref.: https://pydantic-docs.helpmanual.io/usage/models/#generic-models)" - ), +@pytest.mark.parametrize( + "pydantic_version, call_from_python", + product(_PYDANTIC_VERSIONS, [False, True]), ) -def test_generics(tmpdir): - run_test(tmpdir, "generics") +def test_generics(tmp_path: Path, pydantic_version: int, call_from_python: bool): + _run_test(tmp_path, "generics", pydantic_version, call_from_python=call_from_python) -def test_excluding_models(tmpdir): - run_test( - tmpdir, "excluding_models", exclude=("LoginCredentials", "LoginResponseData") +@pytest.mark.parametrize( + "pydantic_version, call_from_python", + product(_PYDANTIC_VERSIONS, [False, True]), +) +def test_excluding_models(tmp_path: Path, pydantic_version: int, call_from_python: bool): + _run_test( + tmp_path, + "excluding_models", + pydantic_version, + call_from_python=call_from_python, + exclude=("LoginCredentials", "LoginResponseData"), ) -def test_relative_filepath(tmpdir): - test_name = "single_module" - relative_path = os.path.join( - ".", "tests", "expected_results", test_name, "input.py" - ) - run_test( - tmpdir, - "single_module", - module_path=relative_path, - ) +@pytest.mark.parametrize( + "pydantic_version, call_from_python", + product([v for v in _PYDANTIC_VERSIONS if v > 1], [False, True]), +) +def test_computed_fields(tmp_path: Path, pydantic_version: int, call_from_python: bool): + _run_test(tmp_path, "computed_fields", pydantic_version, call_from_python=call_from_python) -def test_calling_from_python(tmpdir): - run_test(tmpdir, "single_module", call_from_python=True) - if sys.version_info >= (3, 8): - run_test(tmpdir, "submodules", call_from_python=True) - if sys.version_info >= (3, 7): - run_test(tmpdir, "generics", call_from_python=True) - run_test( - tmpdir, - "excluding_models", - call_from_python=True, - exclude=("LoginCredentials", "LoginResponseData"), +@pytest.mark.parametrize( + "pydantic_version, call_from_python", + product(_PYDANTIC_VERSIONS, [False, True]), +) +def test_extra_fields(tmp_path: Path, pydantic_version: int, call_from_python: bool): + _run_test(tmp_path, "extra_fields", pydantic_version, call_from_python=call_from_python) + + +def test_relative_filepath(tmp_path: Path): + test_name = "single_module" + pydantic_version = _PYDANTIC_VERSIONS[0] + absolute_path = _python_module_path(test_name, pydantic_version) + relative_path = Path(absolute_path).relative_to(Path.cwd()) + _run_test( + tmp_path, + test_name, + pydantic_version, + module_path=str(relative_path), ) -def test_error_if_json2ts_not_installed(tmpdir): - module_path = get_input_module("single_module") - output_path = tmpdir.join(f"cli_single_module.ts").strpath +def test_error_if_json2ts_not_installed(tmp_path: Path): + module_path = _python_module_path("single_module", _PYDANTIC_VERSIONS[0]) + output_path = str(tmp_path / "json2ts_test_output.ts") # If the json2ts command has no spaces and the executable cannot be found, # that means the user either hasn't installed json-schema-to-typescript or they made a typo. @@ -132,11 +150,9 @@ def test_error_if_json2ts_not_installed(tmpdir): assert str(exc2.value).startswith(f'"{invalid_local_cmd}" failed with exit code ') -def test_error_if_invalid_module_path(tmpdir): +def test_error_if_invalid_module_path(tmp_path: Path): with pytest.raises(ModuleNotFoundError): - generate_typescript_defs( - "fake_module", tmpdir.join(f"fake_module_output.ts").strpath - ) + generate_typescript_defs("fake_module", str(tmp_path / "fake_module_output.ts")) def test_parse_cli_args(): diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..c2a51b5 --- /dev/null +++ b/uv.lock @@ -0,0 +1,365 @@ +version = 1 +requires-python = ">=3.8" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690 }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127 }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654 }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598 }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732 }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816 }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325 }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418 }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343 }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136 }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796 }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244 }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279 }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859 }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549 }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477 }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134 }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910 }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348 }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983 }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221 }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371 }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455 }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924 }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252 }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897 }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606 }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373 }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 }, + { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674 }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101 }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554 }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440 }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889 }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142 }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805 }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655 }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296 }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137 }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688 }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120 }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249 }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237 }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311 }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453 }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958 }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938 }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352 }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153 }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pydantic" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/78/58c36d0cf331b659d0ccd99175e3523c457b4f8e67cb92a8fdc22ec1667c/pydantic-2.10.0.tar.gz", hash = "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289", size = 781980 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/ee/255cbfdbf5c47650de70ac8a5425107511f505ed0366c29d537f7f1842e1/pydantic-2.10.0-py3-none-any.whl", hash = "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc", size = 454346 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/cd/8331ae216bcc5a3f2d4c6b941c9f63de647e2700d38133f4f7e0132a00c4/pydantic_core-2.27.0.tar.gz", hash = "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10", size = 412675 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/97/8a42e9c17c305516c0d956a2887d616d3a1b0531b0053ac95a917e4a1ab7/pydantic_core-2.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2ac6b919f7fed71b17fe0b4603c092a4c9b5bae414817c9c81d3c22d1e1bcc", size = 1893954 }, + { url = "https://files.pythonhosted.org/packages/5b/09/ff3ce866f769ebbae2abdcd742247dc2bd6967d646daf54a562ceee6abdb/pydantic_core-2.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e015833384ca3e1a0565a79f5d953b0629d9138021c27ad37c92a9fa1af7623c", size = 1807944 }, + { url = "https://files.pythonhosted.org/packages/88/d7/e04d06ca71a0bd7f4cac24e6aa562129969c91117e5fad2520ede865c8cb/pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72e40628967f6dc572020d04b5f800d71264e0531c6da35097e73bdf38b003", size = 1829151 }, + { url = "https://files.pythonhosted.org/packages/14/24/90b0babb61b68ecc471ce5becad8f7fc5f7835c601774e5de577b051b7ad/pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df45c4073bed486ea2f18757057953afed8dd77add7276ff01bccb79982cf46c", size = 1849502 }, + { url = "https://files.pythonhosted.org/packages/fc/34/62612e655b4d693a6ec515fd0ddab4bfc0cc6759076e09c23fc6966bd07b/pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:836a4bfe0cc6d36dc9a9cc1a7b391265bf6ce9d1eb1eac62ac5139f5d8d9a6fa", size = 2035489 }, + { url = "https://files.pythonhosted.org/packages/12/7d/0ff62235adda41b87c495c1b95c84d4debfecb91cfd62e3100abad9754fa/pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf1340ae507f6da6360b24179c2083857c8ca7644aab65807023cf35404ea8d", size = 2774949 }, + { url = "https://files.pythonhosted.org/packages/7f/ac/e1867e2b808a668f32ad9012eaeac0b0ee377eee8157ab93720f48ee609b/pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ab325fc86fbc077284c8d7f996d904d30e97904a87d6fb303dce6b3de7ebba9", size = 2130123 }, + { url = "https://files.pythonhosted.org/packages/2f/04/5006f2dbf655052826ac8d03d51b9a122de709fed76eb1040aa21772f530/pydantic_core-2.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1da0c98a85a6c6ed702d5556db3b09c91f9b0b78de37b7593e2de8d03238807a", size = 1981988 }, + { url = "https://files.pythonhosted.org/packages/80/8b/bdbe875c4758282402e3cc75fa6bf2f0c8ffac1874f384190034786d3cbc/pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b0202ebf2268954090209a84f9897345719e46a57c5f2c9b7b250ca0a9d3e63", size = 1992043 }, + { url = "https://files.pythonhosted.org/packages/2f/2d/4e46981cfcf4ca4c2ff7734dec08162e398dc598c6c0687454b05a82dc2f/pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:35380671c3c921fe8adf31ad349dc6f7588b7e928dbe44e1093789734f607399", size = 2087309 }, + { url = "https://files.pythonhosted.org/packages/d2/43/56ef2e72360d909629a54198d2bc7ef60f19fde8ceb5c90d7749120d0b61/pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b4c19525c3538fbc0bbda6229f9682fb8199ce9ac37395880e6952798e00373", size = 2140517 }, + { url = "https://files.pythonhosted.org/packages/61/40/81e5d8f84ab070cf091d072bb61b6021ff79d7110b2d0145fe3171b6107b/pydantic_core-2.27.0-cp310-none-win32.whl", hash = "sha256:333c840a1303d1474f491e7be0b718226c730a39ead0f7dab2c7e6a2f3855555", size = 1814120 }, + { url = "https://files.pythonhosted.org/packages/05/64/e543d342b991d38426bcb841bc0b4b95b9bd2191367ba0cc75f258e3d583/pydantic_core-2.27.0-cp310-none-win_amd64.whl", hash = "sha256:99b2863c1365f43f74199c980a3d40f18a218fbe683dd64e470199db426c4d6a", size = 1972268 }, + { url = "https://files.pythonhosted.org/packages/85/ba/5ed9583a44d9fbd6fbc028df8e3eae574a3ef4761d7f56bb4e0eb428d5ce/pydantic_core-2.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d", size = 1891468 }, + { url = "https://files.pythonhosted.org/packages/50/1e/58baa0fde14aafccfcc09a8b45bdc11eb941b58a69536729d832e383bdbd/pydantic_core-2.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386", size = 1807103 }, + { url = "https://files.pythonhosted.org/packages/7d/87/0422a653ddfcf68763eb56d6e4e2ad19df6d5e006f3f4b854fda06ce2ba3/pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea", size = 1827446 }, + { url = "https://files.pythonhosted.org/packages/a4/48/8e431b7732695c93ded79214299a83ac04249d748243b8ba6644ab076574/pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c", size = 1847798 }, + { url = "https://files.pythonhosted.org/packages/98/7d/e1f28e12a26035d7c8b7678830400e5b94129c9ccb74636235a2eeeee40f/pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a", size = 2033797 }, + { url = "https://files.pythonhosted.org/packages/89/b4/ad5bc2b43b7ca8fd5f5068eca7f195565f53911d9ae69925f7f21859a929/pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75", size = 2767592 }, + { url = "https://files.pythonhosted.org/packages/3e/a6/7fb0725eaf1122518c018bfe38aaf4ad3d512e8598e2c08419b9a270f4bf/pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e", size = 2130244 }, + { url = "https://files.pythonhosted.org/packages/a1/2c/453e52a866947a153bb575bbbb6b14db344f07a73b2ad820ff8f40e9807b/pydantic_core-2.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0", size = 1979626 }, + { url = "https://files.pythonhosted.org/packages/7a/43/1faa8601085dab2a37dfaca8d48605b76e38aeefcde58bf95534ab96b135/pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd", size = 1990741 }, + { url = "https://files.pythonhosted.org/packages/dd/ef/21f25f5964979b7e6f9102074083b5448c22c871da438d91db09601e6634/pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b", size = 2086325 }, + { url = "https://files.pythonhosted.org/packages/8a/f9/81e5f910571a20655dd7bf10e6d6db8c279e250bfbdb5ab1a09ce3e0eb82/pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40", size = 2138839 }, + { url = "https://files.pythonhosted.org/packages/59/c4/27917b73d0631098b91f2ec303e1becb823fead0628ee9055fca78ec1e2e/pydantic_core-2.27.0-cp311-none-win32.whl", hash = "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55", size = 1809514 }, + { url = "https://files.pythonhosted.org/packages/ea/48/a30c67d62b8f39095edc3dab6abe69225e8c57186f31cc59a1ab984ea8e6/pydantic_core-2.27.0-cp311-none-win_amd64.whl", hash = "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe", size = 1971838 }, + { url = "https://files.pythonhosted.org/packages/4e/9e/3798b901cf331058bae0ba4712a52fb0106c39f913830aaf71f01fd10d45/pydantic_core-2.27.0-cp311-none-win_arm64.whl", hash = "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206", size = 1862174 }, + { url = "https://files.pythonhosted.org/packages/82/99/43149b127559f3152cd28cb7146592c6547cfe47d528761954e2e8fcabaf/pydantic_core-2.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a", size = 1887064 }, + { url = "https://files.pythonhosted.org/packages/7e/dd/989570c76334aa55ccb4ee8b5e0e6881a513620c6172d93b2f3b77e10f81/pydantic_core-2.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a", size = 1804405 }, + { url = "https://files.pythonhosted.org/packages/3e/b5/bce1d6d6fb71d916c74bf988b7d0cd7fc0c23da5e08bc0d6d6e08c12bf36/pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12", size = 1822595 }, + { url = "https://files.pythonhosted.org/packages/35/93/a6e5e04625ac8fcbed523d7b741e91cc3a37ed1e04e16f8f2f34269bbe53/pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9", size = 1848701 }, + { url = "https://files.pythonhosted.org/packages/3a/74/56ead1436e3f6513b59b3a442272578a6ec09a39ab95abd5ee321bcc8c95/pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f", size = 2031878 }, + { url = "https://files.pythonhosted.org/packages/e1/4d/8905b2710ef653c0da27224bfb6a084b5873ad6fdb975dda837943e5639d/pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a", size = 2673386 }, + { url = "https://files.pythonhosted.org/packages/1d/f0/abe1511f11756d12ce18d016f3555cb47211590e4849ee02e7adfdd1684e/pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4", size = 2152867 }, + { url = "https://files.pythonhosted.org/packages/c7/90/1c588d4d93ce53e1f5ab0cea2d76151fcd36613446bf99b670d7da9ddf89/pydantic_core-2.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840", size = 1986595 }, + { url = "https://files.pythonhosted.org/packages/a3/9c/27d06369f39375966836cde5c8aec0a66dc2f532c13d9aa1a6c370131fbd/pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40", size = 1995731 }, + { url = "https://files.pythonhosted.org/packages/26/4e/b039e52b7f4c51d9fae6715d5d2e47a57c369b8e0cb75838974a193aae40/pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf", size = 2085771 }, + { url = "https://files.pythonhosted.org/packages/01/93/2796bd116a93e7e4e10baca4c55266c4d214b3b4e5ee7f0e9add69c184af/pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef", size = 2150452 }, + { url = "https://files.pythonhosted.org/packages/0f/93/e57562d6ea961557174c3afa481a73ce0e2d8b823e0eb2b320bfb00debbe/pydantic_core-2.27.0-cp312-none-win32.whl", hash = "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379", size = 1830767 }, + { url = "https://files.pythonhosted.org/packages/44/00/4f121ca5dd06420813e7858395b5832603ed0074a5b74ef3104c8dbc2fd5/pydantic_core-2.27.0-cp312-none-win_amd64.whl", hash = "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61", size = 1973909 }, + { url = "https://files.pythonhosted.org/packages/c3/c7/36f87c0dabbde9c0dd59b9024e4bf117a5122515c864ddbe685ed8301670/pydantic_core-2.27.0-cp312-none-win_arm64.whl", hash = "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9", size = 1877037 }, + { url = "https://files.pythonhosted.org/packages/9d/b2/740159bdfe532d856e340510246aa1fd723b97cadf1a38153bdfb52efa28/pydantic_core-2.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85", size = 1886935 }, + { url = "https://files.pythonhosted.org/packages/ca/2a/2f435d9fd591c912ca227f29c652a93775d35d54677b57c3157bbad823b5/pydantic_core-2.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2", size = 1805318 }, + { url = "https://files.pythonhosted.org/packages/ba/f2/755b628009530b19464bb95c60f829b47a6ef7930f8ca1d87dac90fd2848/pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467", size = 1822284 }, + { url = "https://files.pythonhosted.org/packages/3d/c2/a12744628b1b55c5384bd77657afa0780868484a92c37a189fb460d1cfe7/pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10", size = 1848522 }, + { url = "https://files.pythonhosted.org/packages/60/1d/dfcb8ab94a4637d4cf682550a2bf94695863988e7bcbd6f4d83c04178e17/pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc", size = 2031678 }, + { url = "https://files.pythonhosted.org/packages/ee/c8/f9cbcab0275e031c4312223c75d999b61fba60995003cd89dc4866300059/pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d", size = 2672948 }, + { url = "https://files.pythonhosted.org/packages/41/f9/c613546237cf58ed7a7fa9158410c14d0e7e0cbbf95f83a905c9424bb074/pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275", size = 2152419 }, + { url = "https://files.pythonhosted.org/packages/49/71/b951b03a271678b1d1b79481dac38cf8bce8a4e178f36ada0e9aff65a679/pydantic_core-2.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2", size = 1986408 }, + { url = "https://files.pythonhosted.org/packages/9a/2c/07b0d5b5e1cdaa07b7c23e758354377d294ff0395116d39c9fa734e5d89e/pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b", size = 1995895 }, + { url = "https://files.pythonhosted.org/packages/63/09/c21e0d7438c7e742209cc8603607c8d389df96018396c8a2577f6e24c5c5/pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd", size = 2085914 }, + { url = "https://files.pythonhosted.org/packages/68/e4/5ed8f09d92655dcd0a86ee547e509adb3e396cef0a48f5c31e3b060bb9d0/pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3", size = 2150217 }, + { url = "https://files.pythonhosted.org/packages/cd/e6/a202f0e1b81c729130404e82d9de90dc4418ec01df35000d48d027c38501/pydantic_core-2.27.0-cp313-none-win32.whl", hash = "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc", size = 1830973 }, + { url = "https://files.pythonhosted.org/packages/06/3d/21ed0f308e6618ce6c5c6bfb9e71734a9a3256d5474a53c8e5aaaba498ca/pydantic_core-2.27.0-cp313-none-win_amd64.whl", hash = "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0", size = 1974853 }, + { url = "https://files.pythonhosted.org/packages/d7/18/e5744a132b81f98b9f92e15f33f03229a1d254ce7af942b1422ec2ac656f/pydantic_core-2.27.0-cp313-none-win_arm64.whl", hash = "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d", size = 1877469 }, + { url = "https://files.pythonhosted.org/packages/e1/79/9ff7da9e775aa9bf42c9df93fc940d421216b22d255a6edbc11aa291d3f0/pydantic_core-2.27.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e9f9feee7f334b72ceae46313333d002b56f325b5f04271b4ae2aadd9e993ae4", size = 1897587 }, + { url = "https://files.pythonhosted.org/packages/5d/62/fecc64300ea766b6b45de87663ff2adba63c6624a71ba8bc5a323e17ef5e/pydantic_core-2.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:225bfff5d425c34e1fd562cef52d673579d59b967d9de06178850c4802af9039", size = 1777716 }, + { url = "https://files.pythonhosted.org/packages/89/96/85e7daa1151104c24f4b007d32374c899c5e66ebbbf4da4debd1794e084f/pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921ad596ff1a82f9c692b0758c944355abc9f0de97a4c13ca60ffc6d8dc15d4", size = 1831004 }, + { url = "https://files.pythonhosted.org/packages/80/31/a9c66908c95dd2a04d84baa98b46d8ea35abb13354d0a27ac47ffab6decf/pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6354e18a9be37bfa124d6b288a87fb30c673745806c92956f1a25e3ae6e76b96", size = 1850721 }, + { url = "https://files.pythonhosted.org/packages/48/a4/7bc31d7bc5dcbc6d7c8ab2ada38a99d2bd22e93b73e9a9a2a84626016740/pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ee4c2a75af9fe21269a4a0898c5425afb01af1f5d276063f57e2ae1bc64e191", size = 2037703 }, + { url = "https://files.pythonhosted.org/packages/5c/d8/8f68ab9d67c129dc046ad1aa105dc3a86c9ffb6c2243d44d7140381007ea/pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c91e3c04f5191fd3fb68764bddeaf02025492d5d9f23343b283870f6ace69708", size = 2771401 }, + { url = "https://files.pythonhosted.org/packages/8e/e1/bb637cf80583bf9058b8e5a7645cdc99a8adf3941a58329ced63f4c63843/pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ebfac28fd51890a61df36ef202adbd77d00ee5aca4a3dadb3d9ed49cfb929", size = 2133159 }, + { url = "https://files.pythonhosted.org/packages/50/82/c9b7dc0b081a3f26ee321f56b67e5725ec94128d92f1e08525080ba2f2df/pydantic_core-2.27.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36aa167f69d8807ba7e341d67ea93e50fcaaf6bc433bb04939430fa3dab06f31", size = 1983746 }, + { url = "https://files.pythonhosted.org/packages/65/02/6b308344a5968a1b99959fb965e72525837f609adf2412d47769902b2db5/pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e8d89c276234579cd3d095d5fa2a44eb10db9a218664a17b56363cddf226ff3", size = 1992306 }, + { url = "https://files.pythonhosted.org/packages/f2/d6/4f9c7059020863535810a027f993bb384da1f9af60b4d6364493661befb6/pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:5cc822ab90a70ea3a91e6aed3afac570b276b1278c6909b1d384f745bd09c714", size = 2088195 }, + { url = "https://files.pythonhosted.org/packages/80/1e/896a1472a6d7863144e0738181cfdad872c90b57d5c1a5ee073838d751c5/pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e15315691fe2253eb447503153acef4d7223dfe7e7702f9ed66539fcd0c43801", size = 2142683 }, + { url = "https://files.pythonhosted.org/packages/8b/fe/773312dae0be37017e91e2684834bc971aca8f8b6f44e5395c7e4814ae52/pydantic_core-2.27.0-cp38-none-win32.whl", hash = "sha256:dfa5f5c0a4c8fced1422dc2ca7eefd872d5d13eb33cf324361dbf1dbfba0a9fe", size = 1817110 }, + { url = "https://files.pythonhosted.org/packages/90/c1/219e5b3c4dd33d88dee17479b5a3aace3c9c66f26cb7317acc33d74ef02a/pydantic_core-2.27.0-cp38-none-win_amd64.whl", hash = "sha256:513cb14c0cc31a4dfd849a4674b20c46d87b364f997bbcb02282306f5e187abf", size = 1970874 }, + { url = "https://files.pythonhosted.org/packages/00/e4/4d6d9193a33c964920bf56fcbe11fa30511d3d900a81c740b0157579b122/pydantic_core-2.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4148dc9184ab79e356dc00a4199dc0ee8647973332cb385fc29a7cced49b9f9c", size = 1894360 }, + { url = "https://files.pythonhosted.org/packages/f4/46/9d27771309609126678dee81e8e93188dbd0515a543b27e0a01a806c1893/pydantic_core-2.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5fc72fbfebbf42c0856a824b8b0dc2b5cd2e4a896050281a21cfa6fed8879cb1", size = 1773921 }, + { url = "https://files.pythonhosted.org/packages/a0/3a/3a6a4cee7bc11bcb3f8859a63c6b4d88b8df66ad7c9c9e6d667dd894b439/pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:185ef205256cd8b38431205698531026979db89a79587725c1e55c59101d64e9", size = 1829480 }, + { url = "https://files.pythonhosted.org/packages/2b/aa/ecf0fcee9031eef516cef2e336d403a61bd8df75ab17a856bc29f3eb07d4/pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:395e3e1148fa7809016231f8065f30bb0dc285a97b4dc4360cd86e17bab58af7", size = 1849759 }, + { url = "https://files.pythonhosted.org/packages/b6/17/8953bbbe7d3c015bdfa34171ba1738a43682d770e68c87171dd8887035c3/pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33d14369739c5d07e2e7102cdb0081a1fa46ed03215e07f097b34e020b83b1ae", size = 2035679 }, + { url = "https://files.pythonhosted.org/packages/ec/19/514fdf2f684003961b6f34543f0bdf3be2e0f17b8b25cd8d44c343521148/pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7820bb0d65e3ce1e3e70b6708c2f66143f55912fa02f4b618d0f08b61575f12", size = 2773208 }, + { url = "https://files.pythonhosted.org/packages/9a/37/2cdd48b7367fbf0576d16402837212d2b1798aa4ea887f1795f8ddbace07/pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b61989068de9ce62296cde02beffabcadb65672207fc51e7af76dca75e6636", size = 2130616 }, + { url = "https://files.pythonhosted.org/packages/3a/6c/fa100356e1c8f749797d88401a1d5ed8d458705d43e259931681b5b96ab4/pydantic_core-2.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15e350efb67b855cd014c218716feea4986a149ed1f42a539edd271ee074a196", size = 1981857 }, + { url = "https://files.pythonhosted.org/packages/0f/3d/36c0c832c1fd1351c495bf1495b61b2e40248c54f7874e6df439e6ffb9a5/pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:433689845288f9a1ee5714444e65957be26d30915f7745091ede4a83cfb2d7bb", size = 1992515 }, + { url = "https://files.pythonhosted.org/packages/99/12/ee67e29369b368c404c6aead492e1528ec887609d388a7a30b675b969b82/pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:3fd8bc2690e7c39eecdf9071b6a889ce7b22b72073863940edc2a0a23750ca90", size = 2087604 }, + { url = "https://files.pythonhosted.org/packages/0e/6c/72ca869aabe190e4cd36b03226286e430a1076c367097c77cb0704b1cbb3/pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:884f1806609c2c66564082540cffc96868c5571c7c3cf3a783f63f2fb49bd3cd", size = 2141000 }, + { url = "https://files.pythonhosted.org/packages/5c/b8/e7499cfa6f1e46e92a645e74198b7bb9ce3d49e82f626a02726dc917fc74/pydantic_core-2.27.0-cp39-none-win32.whl", hash = "sha256:bf37b72834e7239cf84d4a0b2c050e7f9e48bced97bad9bdf98d26b8eb72e846", size = 1813857 }, + { url = "https://files.pythonhosted.org/packages/2e/27/81203aa6cbf68772afd9c3877ce2e35878f434e824aad4047e7cfd3bc14d/pydantic_core-2.27.0-cp39-none-win_amd64.whl", hash = "sha256:31a2cae5f059329f9cfe3d8d266d3da1543b60b60130d186d9b6a3c20a346361", size = 1974744 }, + { url = "https://files.pythonhosted.org/packages/d3/ad/c1dc814ab524cb247ceb6cb25236895a5cae996c438baf504db610fd6c92/pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4fb49cfdb53af5041aba909be00cccfb2c0d0a2e09281bf542371c5fd36ad04c", size = 1889233 }, + { url = "https://files.pythonhosted.org/packages/24/bb/069a9dd910e6c09aab90a118c08d3cb30dc5738550e9f2d21f3b086352c2/pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:49633583eb7dc5cba61aaf7cdb2e9e662323ad394e543ee77af265736bcd3eaa", size = 1768419 }, + { url = "https://files.pythonhosted.org/packages/cb/a1/f9b4e625ee8c7f683c8295c85d11f79a538eb53719f326646112a7800bda/pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153017e3d6cd3ce979de06d84343ca424bb6092727375eba1968c8b4693c6ecb", size = 1822870 }, + { url = "https://files.pythonhosted.org/packages/12/07/04abaeeabf212650de3edc300b2ab89fb17da9bc4408ef4e01a62efc87dc/pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff63a92f6e249514ef35bc795de10745be0226eaea06eb48b4bbeaa0c8850a4a", size = 1977039 }, + { url = "https://files.pythonhosted.org/packages/0f/9d/99bbeb21d5be1d5affdc171e0e84603a757056f9f4293ef236e41af0a5bc/pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5982048129f40b082c2654de10c0f37c67a14f5ff9d37cf35be028ae982f26df", size = 1974317 }, + { url = "https://files.pythonhosted.org/packages/5f/78/815aa74db1591a9ad4086bc1bf98e2126686245a956d76cd4e72bf9841ad/pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:91bc66f878557313c2a6bcf396e7befcffe5ab4354cfe4427318968af31143c3", size = 1985101 }, + { url = "https://files.pythonhosted.org/packages/d9/a8/9c1557d5282108916448415e85f829b70ba99d97f03cee0e40a296e58a65/pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:68ef5377eb582fa4343c9d0b57a5b094046d447b4c73dd9fbd9ffb216f829e7d", size = 2073399 }, + { url = "https://files.pythonhosted.org/packages/ca/b0/5296273d652fa9aa140771b3f4bb574edd3cbf397090625b988f6a57b02b/pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c5726eec789ee38f2c53b10b1821457b82274f81f4f746bb1e666d8741fcfadb", size = 2129499 }, + { url = "https://files.pythonhosted.org/packages/e9/fd/7f39ff702fdca954f26c84b40d9bf744733bb1a50ca6b7569822b9cbb7f4/pydantic_core-2.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0c431e4be5c1a0c6654e0c31c661cd89e0ca956ef65305c3c3fd96f4e72ca39", size = 1997246 }, + { url = "https://files.pythonhosted.org/packages/bb/4f/76f1ac16a0c277a3a8be2b5b52b0a09929630e794fb1938c4cd85396c34f/pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8e21d927469d04b39386255bf00d0feedead16f6253dcc85e9e10ddebc334084", size = 1889486 }, + { url = "https://files.pythonhosted.org/packages/f3/96/4ff5a8ec0c457afcd87334d4e2f6fd25df6642b4ff8bf587316dd6eccd59/pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b51f964fcbb02949fc546022e56cdb16cda457af485e9a3e8b78ac2ecf5d77e", size = 1768718 }, + { url = "https://files.pythonhosted.org/packages/52/21/e7bab7b9674d5b1a8cf06939929991753e4b814b01bae29321a8739990b3/pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a7fd4de38f7ff99a37e18fa0098c3140286451bc823d1746ba80cec5b433a1", size = 1823291 }, + { url = "https://files.pythonhosted.org/packages/1d/68/d1868a78ce0d776c3e04179fbfa6272e72d4363c49f9bdecfe4b2007dd75/pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fda87808429c520a002a85d6e7cdadbf58231d60e96260976c5b8f9a12a8e13", size = 1977040 }, + { url = "https://files.pythonhosted.org/packages/68/7b/2e361ff81f60c4c28f65b53670436849ec716366d4f1635ea243a31903a2/pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a150392102c402c538190730fda06f3bce654fc498865579a9f2c1d2b425833", size = 1973909 }, + { url = "https://files.pythonhosted.org/packages/a8/44/a4a3718f3b148526baccdb9a0bc8e6b7aa840c796e637805c04aaf1a74c3/pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c9ed88b398ba7e3bad7bd64d66cc01dcde9cfcb7ec629a6fd78a82fa0b559d78", size = 1985091 }, + { url = "https://files.pythonhosted.org/packages/3a/79/2cdf503e8aac926a99d64b2a02642ab1377146999f9a68536c54bd8b2c46/pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:9fe94d9d2a2b4edd7a4b22adcd45814b1b59b03feb00e56deb2e89747aec7bfe", size = 2073484 }, + { url = "https://files.pythonhosted.org/packages/e8/15/74c61b7ea348b252fe97a32e5b531fdde331710db80e9b0fae1302023414/pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d8b5ee4ae9170e2775d495b81f414cc20268041c42571530513496ba61e94ba3", size = 2129473 }, + { url = "https://files.pythonhosted.org/packages/57/81/0e9ebcc80b107e1dfacc677ad7c2ab0202cc0e10ba76b23afbb147ac32fb/pydantic_core-2.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d29e235ce13c91902ef3efc3d883a677655b3908b1cbc73dee816e5e1f8f7739", size = 1997389 }, +] + +[[package]] +name = "pydantic-to-typescript" +version = "2.0.0" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, +] + +[package.optional-dependencies] +dev = [ + { name = "coverage" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "coverage", marker = "extra == 'dev'" }, + { name = "pydantic" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "ruff", marker = "extra == 'dev'" }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + +[[package]] +name = "ruff" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 }, + { url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 }, + { url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 }, + { url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 }, + { url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 }, + { url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 }, + { url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 }, + { url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 }, + { url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 }, + { url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 }, + { url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 }, + { url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 }, + { url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 }, + { url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 }, + { url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 }, + { url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 }, +] + +[[package]] +name = "tomli" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/e4/1b6cbcc82d8832dd0ce34767d5c560df8a3547ad8cbc427f34601415930a/tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8", size = 16622 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f7/4da0ffe1892122c9ea096c57f64c2753ae5dd3ce85488802d11b0992cc6d/tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391", size = 13750 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +]