pax_global_header00006660000000000000000000000064151377357150014530gustar00rootroot0000000000000052 comment=f4475b2b408f1fae1d20da3cd6a148c2544b1c9b json-schema-to-pydantic-0.4.9/000077500000000000000000000000001513773571500162025ustar00rootroot00000000000000json-schema-to-pydantic-0.4.9/.github/000077500000000000000000000000001513773571500175425ustar00rootroot00000000000000json-schema-to-pydantic-0.4.9/.github/workflows/000077500000000000000000000000001513773571500215775ustar00rootroot00000000000000json-schema-to-pydantic-0.4.9/.github/workflows/publish.yaml000066400000000000000000000015221513773571500241310ustar00rootroot00000000000000name: Upload Python Package to PyPI on: release: types: [created] jobs: pypi-publish: name: Publish release to PyPI runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/json-schema-to-pydantic permissions: id-token: write steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v5 with: # Install a specific version of uv. version: "0.7.3" enable-cache: true cache-dependency-glob: "uv.lock" - name: Set up Python run: uv python install - name: Install the project run: uv sync --frozen --all-extras --dev - name: Build package run: uv build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1json-schema-to-pydantic-0.4.9/.github/workflows/test.yaml000066400000000000000000000017531513773571500234500ustar00rootroot00000000000000name: Test on: pull_request: branches: [main] push: branches: [main] jobs: test: name: Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v5 with: version: "0.7.3" enable-cache: true cache-dependency-glob: "uv.lock" - name: Set up Python run: uv python install 3.12 - name: Install dependencies run: uv sync --frozen --all-extras --dev - name: Run tests run: uv run pytest --cov --junitxml=junit.xml -o junit_family=legacy - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Run linters run: uvx ruff check . json-schema-to-pydantic-0.4.9/.gitignore000066400000000000000000000065441513773571500202030ustar00rootroot00000000000000.aider* .env # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. #uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ # PyPI configuration file .pypircjson-schema-to-pydantic-0.4.9/.python-version000066400000000000000000000000051513773571500212020ustar00rootroot000000000000003.12 json-schema-to-pydantic-0.4.9/CONVENTIONS.md000066400000000000000000000000631513773571500203300ustar00rootroot00000000000000- Use Pydantic v2. - Use types everywhere possible.json-schema-to-pydantic-0.4.9/LICENSE000066400000000000000000000020601513773571500172050ustar00rootroot00000000000000MIT License Copyright (c) 2024 Richárd Gyikó Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. json-schema-to-pydantic-0.4.9/README.md000066400000000000000000000064141513773571500174660ustar00rootroot00000000000000# JSON Schema to Pydantic A Python library for automatically generating Pydantic v2 models from JSON Schema definitions. ![PyPI - Version](https://img.shields.io/pypi/v/json-schema-to-pydantic) ![PyPI - Downloads](https://img.shields.io/pypi/dm/json-schema-to-pydantic?logo=pypi) [![codecov](https://codecov.io/github/richard-gyiko/json-schema-to-pydantic/graph/badge.svg?token=YA2Y769H1K)](https://codecov.io/github/richard-gyiko/json-schema-to-pydantic) ## Features - Converts JSON Schema to Pydantic v2 models - Supports complex schema features including: - References ($ref) with circular reference detection - Combiners (allOf, anyOf, oneOf) with proper type discrimination - Type constraints and validations - Array and object validations - Format validations (email, uri, uuid, date-time) - Full type hinting support - Clean, simple API ## Installation ```bash pip install json-schema-to-pydantic ``` ## Development Setup 1. Clone the repository 2. Install development dependencies: ```bash # Using uv (recommended) uv pip install -e ".[dev]" # Or using pip pip install -e ".[dev]" ``` 3. Run tests: ```bash pytest ``` ## Quick Start ```python from json_schema_to_pydantic import create_model # Define your JSON Schema schema = { "title": "User", "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string", "format": "email"}, "age": {"type": "integer", "minimum": 0} }, "required": ["name", "email"] } # Generate your Pydantic model UserModel = create_model(schema) # Use the model user = UserModel( name="John Doe", email="john@example.com", age=30 ) # Example with relaxed validation RelaxedModel = create_model( { "type": "object", "properties": { "tags": {"type": "array"}, # Array without items schema "metadata": {} # Field without type } }, allow_undefined_array_items=True, # Allows arrays without items schema allow_undefined_type=True # Allows fields without type (defaults to Any) ) relaxed_instance = RelaxedModel( tags=[1, "two", True], metadata={"custom": "data"} ) ``` ## Advanced Usage For more complex scenarios, you can use the `PydanticModelBuilder` directly: ```python from json_schema_to_pydantic import PydanticModelBuilder builder = PydanticModelBuilder() model = builder.create_pydantic_model(schema, root_schema) ``` ## Error Handling The library provides specific exceptions for different error cases: ```python from json_schema_to_pydantic import ( SchemaError, # Base class for all schema errors TypeError, # Invalid or unsupported type CombinerError, # Error in schema combiners ReferenceError, # Error in schema references ) try: model = create_model(schema) except TypeError as e: print(f"Invalid type in schema: {e}") except ReferenceError as e: print(f"Invalid reference: {e}") ``` ## Documentation See [docs/features.md](docs/features.md) for detailed documentation of supported JSON Schema features. ## Contributing 1. Fork the repository 2. Create a new branch for your feature 3. Make your changes 4. Run tests and ensure they pass 5. Submit a pull request ## License This project is licensed under the terms of the license included in the repository. json-schema-to-pydantic-0.4.9/docs/000077500000000000000000000000001513773571500171325ustar00rootroot00000000000000json-schema-to-pydantic-0.4.9/docs/features.md000066400000000000000000000123221513773571500212720ustar00rootroot00000000000000# JSON Schema Features Support This document outlines the JSON Schema features supported by the library. ## Basic Types - `string`: Maps to Python `str` - `integer`: Maps to Python `int` - `number`: Maps to Python `float` - `boolean`: Maps to Python `bool` - `array`: Maps to Python `List` - `object`: Maps to Pydantic model ### Multiple Types JSON Schema allows specifying multiple types for a field using an array. The library handles these cases: - Single type with `null`: Maps to `Optional[Type]` ```json {"type": ["string", "null"]} // → Optional[str] ``` - Multiple types without `null`: Maps to `Union[Type1, Type2, ...]` ```json {"type": ["string", "integer"]} // → Union[str, int] ``` - Multiple types with `null`: Maps to `Optional[Union[Type1, Type2, ...]]` ```json {"type": ["string", "integer", "null"]} // → Optional[Union[str, int]] ``` Example with complex types: ```json { "type": "object", "properties": { "flexible_field": { "type": ["string", "integer", "boolean"] }, "optional_flexible": { "type": ["array", "string", "null"], "items": {"type": "integer"} } } } ``` ## String Formats Supported formats with their Python types: - `date-time`: `datetime.datetime` - `uri`: `pydantic.AnyUrl` - `uuid`: `uuid.UUID` Example: ```json { "type": "object", "properties": { "email": {"type": "string", "format": "email"}, "website": {"type": "string", "format": "uri"}, "created_at": {"type": "string", "format": "date-time"} } } ``` ## Constraints ### String Constraints - `minLength`: Minimum string length - `maxLength`: Maximum string length - `pattern`: Regular expression pattern - `const`: Fixed value ### Numeric Constraints - `minimum`/`maximum`: Inclusive bounds - `exclusiveMinimum`/`exclusiveMaximum`: Exclusive bounds - `multipleOf`: Value must be multiple of this number ### Array Constraints - `items`: Schema for array items. By default, this is required for arrays. - `minItems`/`maxItems`: Array length bounds - `uniqueItems`: Enforces unique items ### Handling Arrays Without `items` By default, the library requires arrays to have an `items` schema defined. However, some schemas might omit this. You can allow arrays without a defined `items` schema by passing `allow_undefined_array_items=True` to `create_model` or `PydanticModelBuilder.create_pydantic_model`. When enabled, such arrays will be typed as `List[Any]`. ```python from json_schema_to_pydantic import create_model # Schema with an array lacking 'items' schema = {"type": "object", "properties": {"mixed_tags": {"type": "array"}}} # This would raise a TypeError by default # model = create_model(schema) # Allow arrays without 'items' RelaxedModel = create_model(schema, allow_undefined_array_items=True) # The field 'mixed_tags' will be List[Any] instance = RelaxedModel(mixed_tags=[1, "string", True, None]) ``` ## Schema Combiners ### allOf Combines multiple schemas with AND logic: ```json { "allOf": [ {"type": "object", "properties": {"id": {"type": "integer"}}}, {"type": "object", "properties": {"name": {"type": "string"}}} ] } ``` ### oneOf Creates discriminated unions using a type field: ```json { "oneOf": [ { "type": "object", "properties": { "type": {"const": "user"}, "email": {"type": "string", "format": "email"} } }, { "type": "object", "properties": { "type": {"const": "admin"}, "permissions": {"type": "array", "items": {"type": "string"}} } } ] } ``` ## References Local references (`$ref`) are supported, including within `allOf`, `anyOf`, and `oneOf` combiners. References can be used in any schema location, including as array items and within nested structures. Circular reference detection is also implemented. Example: ```json { "type": "object", "properties": { "parent": {"$ref": "#/definitions/Node"} }, "definitions": { "Node": { "type": "object", "properties": { "children": { "type": "array", "items": {"$ref": "#/definitions/Node"} } } } } } ``` Example with nested references in array items: ```json { "type": "object", "properties": { "items": { "type": "array", "items": {"$ref": "#/definitions/ComplexItem"} } }, "definitions": { "NestedItem": { "type": "object", "properties": { "name": {"type": "string"}, "value": {"type": "integer"} } }, "ComplexItem": { "type": "object", "properties": { "id": {"type": "string"}, "nested_items": { "type": "array", "items": {"$ref": "#/definitions/NestedItem"} } } } } } ``` ## Limitations - External references are not supported - `additionalProperties` defaults to False - `patternProperties` not supported - `if`/`then`/`else` not supported json-schema-to-pydantic-0.4.9/docs/schema.json000066400000000000000000000134141513773571500212700ustar00rootroot00000000000000{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "http://json-schema.org/draft-07/schema#", "title": "Core schema meta-schema", "definitions": { "schemaArray": { "type": "array", "minItems": 1, "items": { "$ref": "#" } }, "nonNegativeInteger": { "type": "integer", "minimum": 0 }, "nonNegativeIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/nonNegativeInteger" }, { "default": 0 } ] }, "simpleTypes": { "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] }, "stringArray": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "default": [] } }, "type": [ "object", "boolean" ], "properties": { "$id": { "type": "string", "format": "uri-reference" }, "$schema": { "type": "string", "format": "uri" }, "$ref": { "type": "string", "format": "uri-reference" }, "$comment": { "type": "string" }, "title": { "type": "string" }, "description": { "type": "string" }, "default": true, "readOnly": { "type": "boolean", "default": false }, "writeOnly": { "type": "boolean", "default": false }, "examples": { "type": "array", "items": true }, "multipleOf": { "type": "number", "exclusiveMinimum": 0 }, "maximum": { "type": "number" }, "exclusiveMaximum": { "type": "number" }, "minimum": { "type": "number" }, "exclusiveMinimum": { "type": "number" }, "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "pattern": { "type": "string", "format": "regex" }, "additionalItems": { "$ref": "#" }, "items": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/schemaArray" } ], "default": true }, "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "uniqueItems": { "type": "boolean", "default": false }, "contains": { "$ref": "#" }, "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "required": { "$ref": "#/definitions/stringArray" }, "additionalProperties": { "$ref": "#" }, "definitions": { "type": "object", "additionalProperties": { "$ref": "#" }, "default": {} }, "properties": { "type": "object", "additionalProperties": { "$ref": "#" }, "default": {} }, "patternProperties": { "type": "object", "additionalProperties": { "$ref": "#" }, "propertyNames": { "format": "regex" }, "default": {} }, "dependencies": { "type": "object", "additionalProperties": { "anyOf": [ { "$ref": "#" }, { "$ref": "#/definitions/stringArray" } ] } }, "propertyNames": { "$ref": "#" }, "const": true, "enum": { "type": "array", "items": true, "minItems": 1, "uniqueItems": true }, "type": { "anyOf": [ { "$ref": "#/definitions/simpleTypes" }, { "type": "array", "items": { "$ref": "#/definitions/simpleTypes" }, "minItems": 1, "uniqueItems": true } ] }, "format": { "type": "string" }, "contentMediaType": { "type": "string" }, "contentEncoding": { "type": "string" }, "if": { "$ref": "#" }, "then": { "$ref": "#" }, "else": { "$ref": "#" }, "allOf": { "$ref": "#/definitions/schemaArray" }, "anyOf": { "$ref": "#/definitions/schemaArray" }, "oneOf": { "$ref": "#/definitions/schemaArray" }, "not": { "$ref": "#" } }, "default": true }json-schema-to-pydantic-0.4.9/pyproject.toml000066400000000000000000000026271513773571500211250ustar00rootroot00000000000000[project] name = "json-schema-to-pydantic" dynamic = ["version"] description = "A Python library for automatically generating Pydantic v2 models from JSON Schema definitions" license = "MIT" # or your chosen license authors = [ { name = "Richard Gyiko", email = "gyiko.richard@outlook.com" } ] keywords = ["json-schema", "pydantic", "validation", "schema", "conversion"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] urls.Homepage = "https://github.com/richard-gyiko/json-schema-to-pydantic" urls.Bug-Tracker = "https://github.com/richard-gyiko/json-schema-to-pydantic/issues" urls.Documentation = "https://github.com/richard-gyiko/json-schema-to-pydantic#readme" readme = "README.md" requires-python = ">=3.9" dependencies = [ "pydantic>=2.10.4", ] [project.optional-dependencies] dev = [ "pytest>=8.3.4", "pytest-cov>=6.0.0", ] [tool.pytest.ini_options] pythonpath = [ "src" ] [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [tool.hatch.version] source = "vcs" fallback-version = "0.0.0"json-schema-to-pydantic-0.4.9/src/000077500000000000000000000000001513773571500167715ustar00rootroot00000000000000json-schema-to-pydantic-0.4.9/src/json_schema_to_pydantic/000077500000000000000000000000001513773571500236575ustar00rootroot00000000000000json-schema-to-pydantic-0.4.9/src/json_schema_to_pydantic/__init__.py000066400000000000000000000037131513773571500257740ustar00rootroot00000000000000import importlib.metadata from .exceptions import ( CombinerError, ReferenceError, SchemaError, TypeError, ) from .model_builder import PydanticModelBuilder try: __version__ = importlib.metadata.version(__name__) except importlib.metadata.PackageNotFoundError: __version__ = "0.0.0" # Fallback for development mode from typing import Any, Dict, Optional, Type, TypeVar from pydantic import BaseModel T = TypeVar("T", bound=BaseModel) def create_model( schema: Dict[str, Any], base_model_type: Type[T] = BaseModel, root_schema: Optional[Dict[str, Any]] = None, allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Type[T]: """ Create a Pydantic model from a JSON Schema. Args: schema: The JSON Schema to convert base_model_type: The base Pydantic model type to use. Defaults to pydantic.BaseModel root_schema: The root schema containing definitions. Defaults to schema if not provided. allow_undefined_array_items: If True, allows arrays without items schema allow_undefined_type: If True, allows schemas without an explicit type populate_by_name: If True, allows population of model fields by name and alias Returns: A Pydantic model class Raises: SchemaError: If the schema is invalid TypeError: If an unsupported type is encountered CombinerError: If there's an error in schema combiners ReferenceError: If there's an error resolving references """ builder = PydanticModelBuilder(base_model_type=base_model_type) return builder.create_pydantic_model( schema, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) __all__ = [ "create_model", "PydanticModelBuilder", "SchemaError", "TypeError", "CombinerError", "ReferenceError", ] json-schema-to-pydantic-0.4.9/src/json_schema_to_pydantic/builders.py000066400000000000000000000100341513773571500260400ustar00rootroot00000000000000from datetime import date, datetime, time from typing import Any, Dict, Literal from uuid import UUID from pydantic import AnyUrl from .interfaces import IConstraintBuilder EMAIL_PATTERN = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" class ConstraintBuilder(IConstraintBuilder): """Builds Pydantic field constraints from JSON Schema""" def build_constraints(self, schema: Dict[str, Any]) -> Dict[str, Any]: """Extract field constraints from schema.""" constraints = {} # String constraints if "minLength" in schema: constraints["min_length"] = schema["minLength"] if "maxLength" in schema: constraints["max_length"] = schema["maxLength"] if "pattern" in schema: constraints["pattern"] = schema["pattern"] # Handle const values first if "const" in schema: return Literal[schema["const"]] if "format" in schema: format_type = schema["format"] if format_type == "email": constraints["pattern"] = EMAIL_PATTERN return constraints elif format_type == "date-time": return datetime elif format_type == "date": return date elif format_type == "time": return time elif format_type == "uri": return AnyUrl elif format_type == "uuid": return UUID # Number constraints if "minimum" in schema: constraints["ge"] = schema["minimum"] if "maximum" in schema: constraints["le"] = schema["maximum"] if "exclusiveMinimum" in schema: constraints["gt"] = schema["exclusiveMinimum"] if "exclusiveMaximum" in schema: constraints["lt"] = schema["exclusiveMaximum"] if "multipleOf" in schema: constraints["multiple_of"] = schema["multipleOf"] # Array constraints if "minItems" in schema: constraints["min_length"] = schema["minItems"] if "maxItems" in schema: constraints["max_length"] = schema["maxItems"] return constraints def merge_constraints( self, schema1: Dict[str, Any], schema2: Dict[str, Any] ) -> Dict[str, Any]: """Merges constraints from two schemas for the same property.""" merged = schema1.copy() # Handle numeric constraints for constraint in [ "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf", ]: if constraint in schema2: if constraint in merged: if "minimum" in constraint or "exclusiveMinimum" in constraint: merged[constraint] = max( merged[constraint], schema2[constraint] ) else: merged[constraint] = min( merged[constraint], schema2[constraint] ) else: merged[constraint] = schema2[constraint] # Handle string constraints for constraint in ["minLength", "maxLength", "pattern"]: if constraint in schema2: if constraint in merged: if "min" in constraint.lower(): merged[constraint] = max( merged[constraint], schema2[constraint] ) elif "max" in constraint.lower(): merged[constraint] = min( merged[constraint], schema2[constraint] ) else: # For pattern, combine with AND logic merged[constraint] = ( f"(?={merged[constraint]})(?={schema2[constraint]})" ) else: merged[constraint] = schema2[constraint] return merged json-schema-to-pydantic-0.4.9/src/json_schema_to_pydantic/exceptions.py000066400000000000000000000005151513773571500264130ustar00rootroot00000000000000class SchemaError(Exception): """Base class for schema-related errors""" pass class TypeError(SchemaError): """Invalid or unsupported type""" pass class CombinerError(SchemaError): """Error in schema combiners""" pass class ReferenceError(SchemaError): """Error in schema references""" pass json-schema-to-pydantic-0.4.9/src/json_schema_to_pydantic/handlers.py000066400000000000000000000275051513773571500260420ustar00rootroot00000000000000from typing import Annotated, Any, Callable, Dict, List, Literal, Type, Union from pydantic import ( BaseModel, ConfigDict, Discriminator, Field, RootModel, create_model, ) from .exceptions import CombinerError from .interfaces import ( ICombinerHandler, IConstraintBuilder, IReferenceResolver, ITypeResolver, ) class CombinerHandler(ICombinerHandler): """Handles JSON Schema combiners (allOf, anyOf, oneOf)""" def __init__( self, type_resolver: ITypeResolver, constraint_builder: IConstraintBuilder, reference_resolver: IReferenceResolver, recursive_field_builder: Callable, field_info_builder: Callable, name_sanitizer: Callable, ): # Store injected dependencies self.type_resolver = type_resolver self.constraint_builder = constraint_builder self.reference_resolver = reference_resolver # Store callbacks for recursive building self.recursive_field_builder = recursive_field_builder self.field_info_builder = field_info_builder self.name_sanitizer = name_sanitizer def handle_all_of( self, schemas: List[Dict[str, Any]], root_schema: Dict[str, Any], allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Type[BaseModel]: """Combines multiple schemas with AND logic.""" if not schemas: raise CombinerError("allOf must contain at least one schema") merged_properties = {} required_fields = set() for schema in schemas: if not isinstance(schema, dict): raise CombinerError(f"Invalid schema in allOf: {schema}") # Resolve top-level $ref if "$ref" in schema: schema = self.reference_resolver.resolve_ref( schema["$ref"], schema, root_schema ) properties = schema.get("properties", {}) required = schema.get("required", []) for prop_name, prop_schema in properties.items(): if prop_name in merged_properties: # Merge constraints for existing property using the injected constraint_builder merged_properties[prop_name] = ( self.constraint_builder.merge_constraints( merged_properties[prop_name], prop_schema ) ) else: merged_properties[prop_name] = prop_schema required_fields.update(required) # Build field definitions using the callbacks field_definitions = {} for name, prop_schema in merged_properties.items(): sanitized_name, alias = self.name_sanitizer(name, set(merged_properties)) field_type = self.recursive_field_builder( prop_schema, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) field_info = self.field_info_builder(prop_schema, name in required_fields, alias=alias) field_definitions[sanitized_name] = (field_type, field_info) return create_model( "AllOfModel", __config__=ConfigDict(extra="forbid", populate_by_name=populate_by_name), **field_definitions, ) def handle_any_of( self, schemas: List[Dict[str, Any]], root_schema: Dict[str, Any], allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Any: """Allows validation against any of the given schemas.""" if not schemas: raise CombinerError("anyOf must contain at least one schema") possible_types = [] for schema in schemas: if not isinstance(schema, dict): raise CombinerError(f"Invalid schema in anyOf: {schema}") # Resolve $ref if present if "$ref" in schema: schema = self.reference_resolver.resolve_ref( schema["$ref"], schema, root_schema ) # Use the recursive_field_builder callback to resolve the type resolved_type = self.recursive_field_builder( schema, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) possible_types.append(resolved_type) return Union[tuple(possible_types)] def handle_one_of( self, schemas: List[Dict[str, Any]], root_schema: Dict[str, Any], allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Any: """ Handles oneOf schema combiner with support for multiple patterns: - Const/literal unions: {"oneOf": [{"const": "a"}, {"const": "b"}]} - Simple type unions: {"oneOf": [{"type": "integer"}, {"type": "string"}]} - Discriminated unions: objects with a "type" const property - General unions: fallback to Union type for any other schemas """ if not schemas: raise CombinerError("oneOf must contain at least one schema") # Check for const/literal union pattern # Example: {"oneOf": [{"const": "a"}, {"const": "b"}]} if all(isinstance(s, dict) and "const" in s for s in schemas): const_values = tuple(s["const"] for s in schemas) # Only use Literal if all const values are valid Literal types # (str, int, bool, bytes, None, Enum members) if all( isinstance(v, (str, int, bool, bytes, type(None))) for v in const_values ): return Literal[const_values] # Fall through to general union handling for complex const types # Check for discriminated union pattern (objects with type const) if self._is_discriminated_union(schemas, root_schema): return self._handle_discriminated_union( schemas, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) # Fallback: treat as general union (like anyOf) # This handles simple type unions, refs without discriminators, and mixed schemas return self._handle_union( schemas, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) def _is_discriminated_union( self, schemas: List[Dict[str, Any]], root_schema: Dict[str, Any] ) -> bool: """Check if all schemas are objects with a type const discriminator.""" for schema in schemas: if not isinstance(schema, dict): return False # Resolve $ref if present resolved = schema if "$ref" in schema: resolved = self.reference_resolver.resolve_ref( schema["$ref"], schema, root_schema ) # Must have properties with a type const properties = resolved.get("properties", {}) type_prop = properties.get("type", {}) if not isinstance(type_prop, dict) or "const" not in type_prop: return False return True def _handle_discriminated_union( self, schemas: List[Dict[str, Any]], root_schema: Dict[str, Any], allow_undefined_array_items: bool, allow_undefined_type: bool, populate_by_name: bool, ) -> Type[BaseModel]: """Handle oneOf with discriminated union pattern (objects with type const).""" variant_models = {} for variant_schema in schemas: if not isinstance(variant_schema, dict): raise CombinerError(f"Invalid schema in oneOf: {variant_schema}") # Resolve $ref if present at the variant level ref_path = None if "$ref" in variant_schema: ref_path = variant_schema["$ref"] variant_schema = self.reference_resolver.resolve_ref( ref_path, variant_schema, root_schema ) properties = variant_schema.get("properties", {}) type_const = properties.get("type", {}).get("const") # Create field definitions for this variant fields = {} required = variant_schema.get("required", []) for name, prop_schema in properties.items(): if name == "type": description = prop_schema.get("description") fields[name] = ( Literal[type_const], Field(default=type_const, description=description), ) elif "oneOf" in prop_schema: sanitized_name, alias = self.name_sanitizer(name, set(properties)) field_type = self.recursive_field_builder( prop_schema, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) field_info = self.field_info_builder(prop_schema, name in required, alias=alias) fields[sanitized_name] = (field_type, field_info) elif name != "type": sanitized_name, alias = self.name_sanitizer(name, set(properties)) field_type = self.recursive_field_builder( prop_schema, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) field_info = self.field_info_builder(prop_schema, name in required, alias=alias) fields[sanitized_name] = (field_type, field_info) # Use the name from the $ref if available, otherwise generate one if ref_path: model_name = ref_path.split("/")[-1] else: model_name = f"Variant_{type_const}" variant_model = create_model( model_name, __config__=ConfigDict(extra="forbid", populate_by_name=populate_by_name), **fields, ) variant_models[type_const] = variant_model # Always wrap in RootModel for consistent access pattern if len(variant_models) == 1: return RootModel[list(variant_models.values())[0]] else: union_type = Annotated[ Union[tuple(variant_models.values())], Discriminator(discriminator="type"), ] return RootModel[union_type] def _handle_union( self, schemas: List[Dict[str, Any]], root_schema: Dict[str, Any], allow_undefined_array_items: bool, allow_undefined_type: bool, populate_by_name: bool, ) -> Any: """Handle oneOf as a union type (like anyOf).""" possible_types = [] for schema in schemas: if not isinstance(schema, dict): raise CombinerError(f"Invalid schema in oneOf: {schema}") # Let recursive_field_builder handle $ref resolution resolved_type = self.recursive_field_builder( schema, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) possible_types.append(resolved_type) return Union[tuple(possible_types)] json-schema-to-pydantic-0.4.9/src/json_schema_to_pydantic/interfaces.py000066400000000000000000000045611513773571500263620ustar00rootroot00000000000000from abc import ABC, abstractmethod from typing import Any, Dict, Generic, List, Optional, Type, TypeVar from pydantic import BaseModel T = TypeVar("T", bound=BaseModel) class ITypeResolver(ABC): @abstractmethod def resolve_type( self, schema: Dict[str, Any], root_schema: Dict[str, Any], allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Any: """Resolves JSON Schema types to Pydantic types""" pass class IConstraintBuilder(ABC): @abstractmethod def build_constraints(self, schema: Dict[str, Any]) -> Dict[str, Any]: """Builds Pydantic field constraints from JSON Schema""" pass class ICombinerHandler(ABC): @abstractmethod def handle_all_of( self, schemas: List[Dict[str, Any]], root_schema: Dict[str, Any], allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Any: """Handles allOf combiner""" pass @abstractmethod def handle_any_of( self, schemas: List[Dict[str, Any]], root_schema: Dict[str, Any], allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Any: """Handles anyOf combiner""" pass @abstractmethod def handle_one_of( self, schemas: List[Dict[str, Any]], root_schema: Dict[str, Any], allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Any: """Handles oneOf combiner""" pass class IReferenceResolver(ABC): @abstractmethod def resolve_ref( self, ref: str, schema: Dict[str, Any], root_schema: Dict[str, Any] ) -> Any: """Resolves JSON Schema references""" pass class IModelBuilder(ABC, Generic[T]): @abstractmethod def create_pydantic_model( self, schema: Dict[str, Any], root_schema: Optional[Dict[str, Any]] = None, allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Type[T]: """Creates a Pydantic model from JSON Schema""" pass json-schema-to-pydantic-0.4.9/src/json_schema_to_pydantic/model_builder.py000066400000000000000000000540711513773571500270460ustar00rootroot00000000000000from typing import Annotated, Any, Dict, List, Optional, Set, Type, TypeVar from pydantic import BaseModel, ConfigDict, Field, RootModel, create_model from . import SchemaError from .builders import ConstraintBuilder from .handlers import CombinerHandler from .interfaces import IModelBuilder from .resolvers import ReferenceResolver, TypeResolver T = TypeVar("T", bound=BaseModel) class PydanticModelBuilder(IModelBuilder[T]): """Creates Pydantic models from JSON Schema definitions""" # Standard JSON Schema properties for fields STANDARD_FIELD_PROPERTIES = { "type", "format", "description", "default", "title", "examples", "const", "enum", "multipleOf", "maximum", "exclusiveMaximum", "minimum", "exclusiveMinimum", "maxLength", "minLength", "pattern", "items", "additionalItems", "maxItems", "minItems", "uniqueItems", "properties", "additionalProperties", "required", "patternProperties", "dependencies", "propertyNames", "if", "then", "else", "allOf", "anyOf", "oneOf", "not", "$ref", "$defs", "definitions", } # Standard JSON Schema properties for models STANDARD_MODEL_PROPERTIES = { "type", "title", "description", "properties", "required", "additionalProperties", "patternProperties", "dependencies", "propertyNames", "if", "then", "else", "allOf", "anyOf", "oneOf", "not", "$ref", "$defs", "definitions", "$schema", "$id", "$comment", "items", "minItems", "maxItems", "uniqueItems", } def __init__(self, base_model_type: Type[T] = BaseModel): # Instantiate resolvers and builders directly self.type_resolver = TypeResolver() self.constraint_builder = ConstraintBuilder() self.reference_resolver = ReferenceResolver() # Pass resolvers and method references as callbacks to CombinerHandler self.combiner_handler = CombinerHandler( type_resolver=self.type_resolver, constraint_builder=self.constraint_builder, reference_resolver=self.reference_resolver, recursive_field_builder=self._get_field_type, field_info_builder=self._build_field_info, name_sanitizer=self._sanitize_field_name, ) self.base_model_type = base_model_type # Track models being built to handle recursive references self._model_cache: Dict[str, Type[BaseModel]] = {} self._building_models: Set[str] = set() self._models_to_rebuild: Set[Type[BaseModel]] = set() def create_pydantic_model( self, schema: Dict[str, Any], root_schema: Optional[Dict[str, Any]] = None, allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, _schema_ref: Optional[str] = None, ) -> Type[T]: """ Creates a Pydantic model from a JSON Schema definition. Args: schema: The JSON Schema to convert root_schema: The root schema containing definitions allow_undefined_array_items: If True, allows arrays without items schema allow_undefined_type: If True, allows schemas without an explicit type populate_by_name: If True, allows population of model fields by name and alias _schema_ref: Internal parameter to track the original $ref of the schema Returns: A Pydantic model class """ if root_schema is None: root_schema = schema # Store the original ref if present for tracking # Use the passed ref or extract from schema original_ref = _schema_ref or schema.get("$ref") # Handle references if "$ref" in schema: # Check if we've already built this model if original_ref in self._model_cache: return self._model_cache[original_ref] # Mark this ref as being built if original_ref: self._building_models.add(original_ref) schema = self.reference_resolver.resolve_ref( schema["$ref"], schema, root_schema, ) # Handle combiners if "allOf" in schema: return self.combiner_handler.handle_all_of( schema["allOf"], root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) if "anyOf" in schema: return self.combiner_handler.handle_any_of( schema["anyOf"], root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) if "oneOf" in schema: return self.combiner_handler.handle_one_of( schema["oneOf"], root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name ) # Handle top-level arrays if schema.get("type") == "array": return self._create_array_root_model( schema, root_schema, allow_undefined_array_items, original_ref ) # Handle top-level scalars # We check for explicit scalar types or enum/const schema_type = schema.get("type") is_scalar = ( ( isinstance(schema_type, str) and schema_type in ("string", "integer", "number", "boolean", "null") ) or ( isinstance(schema_type, list) and "object" not in schema_type and "array" not in schema_type ) or "enum" in schema or "const" in schema ) if is_scalar: return self._create_scalar_root_model( schema, root_schema, allow_undefined_array_items, allow_undefined_type, original_ref, ) # Get model properties # If this schema is referenced, use the ref name as the title if no title is provided if original_ref and "title" not in schema: ref_parts = original_ref.split("/") title = ref_parts[-1] if ref_parts else "DynamicModel" else: title = schema.get("title", "DynamicModel") description = schema.get("description") properties = schema.get("properties", {}) required = schema.get("required", []) # Extract model-level json_schema_extra model_extra = { key: value for key, value in schema.items() if key not in self.STANDARD_MODEL_PROPERTIES } # Build field definitions fields = {} for field_name, field_schema in properties.items(): field_type = self._get_field_type( field_schema, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) model_field_name, alias = self._sanitize_field_name(field_name, set(properties)) field_info = self._build_field_info(field_schema, field_name in required, alias=alias) fields[model_field_name] = (field_type, field_info) # Create the model with or without json_schema_extra if model_extra: # Create a dynamic base class with the config class DynamicBase(self.base_model_type): model_config = ConfigDict( json_schema_extra=model_extra, populate_by_name=populate_by_name, ) model = create_model(title, __base__=DynamicBase, **fields) else: model = create_model( title, __base__=self.base_model_type, __config__=ConfigDict(populate_by_name=populate_by_name), **fields, ) if description: model.__doc__ = description # Cache the model if it was referenced if original_ref: self._model_cache[original_ref] = model # Remove from building set self._building_models.discard(original_ref) # Mark model for rebuild after all models are created self._models_to_rebuild.add(model) # If this is the top-level call (no models being built), rebuild all models if not self._building_models and self._models_to_rebuild: # Build namespace with all models namespace = {m.__name__: m for m in self._models_to_rebuild} # Rebuild all models to resolve forward references for m in self._models_to_rebuild: m.model_rebuild(_types_namespace=namespace) # Clear the rebuild set self._models_to_rebuild.clear() return model def _create_array_root_model( self, schema: Dict[str, Any], root_schema: Dict[str, Any], allow_undefined_array_items: bool, original_ref: Optional[str], ) -> Type[T]: """ Creates a RootModel for top-level array schemas. Args: schema: The array schema definition root_schema: The root schema containing definitions allow_undefined_array_items: Whether to allow arrays without items original_ref: The original reference if this was a $ref Returns: A RootModel class that validates arrays """ # Get the title for the model if original_ref and "title" not in schema: ref_parts = original_ref.split("/") title = ref_parts[-1] if ref_parts else "DynamicModel" else: title = schema.get("title", "DynamicModel") # Get description description = schema.get("description") # Resolve the item type items_schema = schema.get("items") if not items_schema: if allow_undefined_array_items: item_type = Any else: from .exceptions import TypeError raise TypeError("Array type must specify 'items' schema") else: item_type = self._get_field_type( items_schema, root_schema, allow_undefined_array_items ) # Determine if we need to use Set or List if schema.get("uniqueItems", False): array_type = Set[item_type] else: array_type = List[item_type] # Build constraints for the array constraints = self.constraint_builder.build_constraints(schema) # Extract model-level json_schema_extra (non-standard properties) model_extra = { key: value for key, value in schema.items() if key not in self.STANDARD_MODEL_PROPERTIES } # Create the RootModel class dynamically # RootModel requires the type to be specified as a generic parameter # We create a class that properly inherits from RootModel[array_type] # Build the class namespace namespace = {} if description: namespace["__doc__"] = description # Add model_config if we have extra properties if model_extra: namespace["model_config"] = ConfigDict(json_schema_extra=model_extra) # Apply constraints to the root field using Annotated if constraints: # Use Annotated to add Field constraints to the array type root_type = Annotated[array_type, Field(**constraints)] else: root_type = array_type namespace["__annotations__"] = {"root": root_type} # Create the RootModel subclass model = type(title, (RootModel[array_type],), namespace) # Cache the model if it was referenced if original_ref: self._model_cache[original_ref] = model # Mark model for rebuild if needed self._models_to_rebuild.add(model) # Rebuild models if this is the top-level call if not self._building_models and self._models_to_rebuild: namespace = {m.__name__: m for m in self._models_to_rebuild} for m in self._models_to_rebuild: m.model_rebuild(_types_namespace=namespace) self._models_to_rebuild.clear() return model def _create_scalar_root_model( self, schema: Dict[str, Any], root_schema: Dict[str, Any], allow_undefined_array_items: bool, allow_undefined_type: bool, original_ref: Optional[str], ) -> Type[T]: """ Creates a RootModel for top-level scalar schemas (string, integer, etc). Args: schema: The JSON Schema fragment describing the scalar value. root_schema: The root JSON Schema document containing shared definitions. allow_undefined_array_items: Whether to allow array items without an explicit type when resolving nested schemas. allow_undefined_type: Whether to allow schemas that do not declare an explicit ``type`` field when resolving the scalar type. original_ref: The original JSON Schema ``$ref`` string for this schema, if any. Returns: Type[T]: A Pydantic ``RootModel`` subclass that represents the scalar schema. """ # Get the title for the model if original_ref and "title" not in schema: ref_parts = original_ref.split("/") title = ref_parts[-1] if ref_parts else "DynamicModel" else: title = schema.get("title", "DynamicModel") description = schema.get("description") # Resolve the scalar type using existing logic scalar_type = self.type_resolver.resolve_type( schema, root_schema, allow_undefined_array_items, allow_undefined_type ) # Build constraints constraints = self.constraint_builder.build_constraints(schema) # Extract model-level json_schema_extra model_extra = { key: value for key, value in schema.items() if key not in self.STANDARD_MODEL_PROPERTIES } # Build the class namespace namespace = {} if description: namespace["__doc__"] = description if model_extra: namespace["model_config"] = ConfigDict(json_schema_extra=model_extra) # Apply constraints using Annotated if needed if isinstance(constraints, dict) and constraints: root_type = Annotated[scalar_type, Field(**constraints)] else: root_type = scalar_type namespace["__annotations__"] = {"root": root_type} model = type(title, (RootModel[scalar_type],), namespace) if original_ref: self._model_cache[original_ref] = model self._models_to_rebuild.add(model) if not self._building_models and self._models_to_rebuild: ns = {m.__name__: m for m in self._models_to_rebuild} for m in self._models_to_rebuild: m.model_rebuild(_types_namespace=ns) self._models_to_rebuild.clear() return model def _get_field_type( self, field_schema: Dict[str, Any], root_schema: Dict[str, Any], allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Any: """Resolves the Python type for a field schema.""" # Store the original ref if present original_ref = field_schema.get("$ref") if "$ref" in field_schema: # Check if this reference is already being built (recursive reference) if original_ref in self._building_models: # Return cached model if available if original_ref in self._model_cache: return self._model_cache[original_ref] # Otherwise, return a forward reference (string) # Extract model name from ref ref_parts = original_ref.split("/") model_name = ref_parts[-1] if ref_parts else "DynamicModel" return model_name # Check if we've already built this model if original_ref in self._model_cache: return self._model_cache[original_ref] # Mark this ref as being built before resolving self._building_models.add(original_ref) field_schema = self.reference_resolver.resolve_ref( field_schema["$ref"], field_schema, root_schema ) # Handle combiners if "allOf" in field_schema: return self.combiner_handler.handle_all_of( field_schema["allOf"], root_schema, allow_undefined_array_items, allow_undefined_type, ) if "anyOf" in field_schema: return self.combiner_handler.handle_any_of( field_schema["anyOf"], root_schema, allow_undefined_array_items, allow_undefined_type, ) if "oneOf" in field_schema: return self.combiner_handler.handle_one_of( field_schema["oneOf"], root_schema, allow_undefined_array_items, allow_undefined_type, ) # Handle arrays by recursively processing items if field_schema.get("type") == "array": items_schema = field_schema.get("items") if not items_schema: if allow_undefined_array_items: return List[Any] else: from .exceptions import TypeError raise TypeError("Array type must specify 'items' schema") # Recursively process the items schema through the model builder # This ensures that object types get proper models created item_type = self._get_field_type( items_schema, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, ) if field_schema.get("uniqueItems", False): return Set[item_type] return List[item_type] # Handle nested objects by recursively creating models if field_schema.get("type") == "object" and "properties" in field_schema: # Build the model and cache/cleanup if we tracked this ref # Pass the original ref so the model can be named correctly model = self.create_pydantic_model( field_schema, root_schema, allow_undefined_array_items, allow_undefined_type, populate_by_name, _schema_ref=original_ref, ) # If we were tracking a ref for this field, cache and cleanup if original_ref and original_ref in self._building_models: self._model_cache[original_ref] = model self._building_models.discard(original_ref) # Mark model for rebuild after all models are created self._models_to_rebuild.add(model) return model return self.type_resolver.resolve_type( schema=field_schema, root_schema=root_schema, allow_undefined_array_items=allow_undefined_array_items, allow_undefined_type=allow_undefined_type, ) def _build_field_info( self, field_schema: Dict[str, Any], required: bool, alias: Optional[str] = None, ) -> Field: """Creates a Pydantic Field with constraints from schema. If the field_name is invalid as a model field name, it adds the original name as an alias. """ field_kwargs = {} # Add constraints constraints = self.constraint_builder.build_constraints(field_schema) if isinstance(constraints, type): pass # Type will be handled by type_resolver elif isinstance(constraints, dict): field_kwargs.update(constraints) # Handle description if "description" in field_schema: field_kwargs["description"] = field_schema["description"] # Handle default value if "default" in field_schema: field_kwargs["default"] = field_schema["default"] elif not required: field_kwargs["default"] = None # Handle alias if alias: field_kwargs["alias"] = alias # Extract field-level json_schema_extra field_extra = { key: value for key, value in field_schema.items() if key not in self.STANDARD_FIELD_PROPERTIES } if field_extra: field_kwargs["json_schema_extra"] = field_extra return Field(**field_kwargs) @staticmethod def _sanitize_field_name(field_name: str, invalid_names: set) -> tuple[str, Optional[str]]: """Sanitizes field names to be valid Pydantic field names. Returns the sanitized field name and an optional alias if the name was changed. """ # If the sanitized name is different, return the alias sanitized_name = field_name alias = None if field_name.startswith("_"): sanitized_name, alias = field_name[1:], field_name if sanitized_name in invalid_names: raise SchemaError( f"Duplicate field name after sanitization: '{sanitized_name}'\nPydantic does " "not support field names starting with underscores when another field would " "result in the same name." ) return sanitized_name, aliasjson-schema-to-pydantic-0.4.9/src/json_schema_to_pydantic/models.py000066400000000000000000000015341513773571500255170ustar00rootroot00000000000000from dataclasses import dataclass from typing import List, Optional @dataclass class SchemaType: """Represents a JSON Schema type""" name: str format: Optional[str] = None @dataclass class FieldConstraints: """Represents JSON Schema field constraints""" min_length: Optional[int] = None max_length: Optional[int] = None pattern: Optional[str] = None minimum: Optional[float] = None maximum: Optional[float] = None exclusive_minimum: Optional[float] = None exclusive_maximum: Optional[float] = None multiple_of: Optional[float] = None min_items: Optional[int] = None max_items: Optional[int] = None unique_items: bool = False @dataclass class CombinerSchema: """Represents a JSON Schema combiner""" type: str # 'allOf', 'anyOf', 'oneOf' schemas: List[dict] root_schema: dict json-schema-to-pydantic-0.4.9/src/json_schema_to_pydantic/resolvers.py000066400000000000000000000171071513773571500262630ustar00rootroot00000000000000from datetime import date, datetime, time from typing import Any, List, Literal, Optional, Set, Union from uuid import UUID from pydantic import AnyUrl from .exceptions import ReferenceError, TypeError from .interfaces import IReferenceResolver, ITypeResolver class TypeResolver(ITypeResolver): """Resolves JSON Schema types to Pydantic types""" def resolve_type( self, schema: dict, root_schema: dict, allow_undefined_array_items: bool = False, allow_undefined_type: bool = False, populate_by_name: bool = False, ) -> Any: """Get the Pydantic field type for a JSON schema field.""" if not isinstance(schema, dict): raise TypeError(f"Invalid schema: expected dict, got {type(schema)}") # Handle references first if "$ref" in schema: reference_resolver = ReferenceResolver() schema = reference_resolver.resolve_ref(schema["$ref"], schema, root_schema) if "const" in schema: if schema["const"] is None: return type(None) return Literal[schema["const"]] if schema.get("type") == "null": return type(None) # Handle array of types (e.g. ["string", "null"]) if isinstance(schema.get("type"), list): types = schema["type"] if "null" in types: other_types = [t for t in types if t != "null"] if len(other_types) == 0: # Only null: {"type": ["null"]} return type(None) elif len(other_types) == 1: return Optional[ self.resolve_type( schema={**schema, **{"type": other_types[0]}}, root_schema=root_schema, allow_undefined_array_items=allow_undefined_array_items, allow_undefined_type=allow_undefined_type, ) ] else: # Multiple types with null: Union[type1, type2, ...] | None resolved_types = [ self.resolve_type( schema={**schema, **{"type": t}}, root_schema=root_schema, allow_undefined_array_items=allow_undefined_array_items, allow_undefined_type=allow_undefined_type, ) for t in other_types ] return Optional[Union[tuple(resolved_types)]] else: # No null in types if len(types) == 1: # Single type in array: {"type": ["string"]} return self.resolve_type( schema={**schema, **{"type": types[0]}}, root_schema=root_schema, allow_undefined_array_items=allow_undefined_array_items, allow_undefined_type=allow_undefined_type, ) else: # Multiple types without null: Union[type1, type2, ...] resolved_types = [ self.resolve_type( schema={**schema, **{"type": t}}, root_schema=root_schema, allow_undefined_array_items=allow_undefined_array_items, allow_undefined_type=allow_undefined_type, ) for t in types ] return Union[tuple(resolved_types)] if "enum" in schema: if not schema["enum"]: raise TypeError("Enum must have at least one value") return Literal[tuple(schema["enum"])] # Infer type if not explicitly specified schema_type = schema.get("type") if not schema_type: # Infer type based on schema structure if "properties" in schema: schema_type = "object" elif "items" in schema: schema_type = "array" elif allow_undefined_type: schema_type = "anyType" else: raise TypeError("Schema must specify a type. Set allow_undefined_type=True to infer Any type for schemas without explicit types.") if schema_type == "array": items_schema = schema.get("items") if not items_schema: if allow_undefined_array_items: return List[Any] # Allow any type if items are not defined else: raise TypeError("Array type must specify 'items' schema") # Handle references in array items if isinstance(items_schema, dict) and "$ref" in items_schema: # We need to resolve the reference before proceeding reference_resolver = ReferenceResolver() items_schema = reference_resolver.resolve_ref( items_schema["$ref"], items_schema, root_schema ) item_type = self.resolve_type( schema=items_schema, root_schema=root_schema, allow_undefined_array_items=allow_undefined_array_items, allow_undefined_type=allow_undefined_type, ) if schema.get("uniqueItems", False): return Set[item_type] return List[item_type] # Handle format for string types if schema_type == "string" and "format" in schema: format_type = schema["format"] format_map = { "date-time": datetime, "date": date, "time": time, "email": str, "uri": AnyUrl, "uuid": UUID, } return format_map.get(format_type, str) if schema_type == "anyType": return Any type_map = { "string": str, "integer": int, "number": float, "boolean": bool, "object": dict, # Will be replaced with actual model in builder } return type_map.get(schema_type, str) class ReferenceResolver(IReferenceResolver): """Resolves JSON Schema references""" def __init__(self): self._processing_refs: Set[str] = set() def resolve_ref(self, ref: str, schema: dict, root_schema: dict) -> Any: """Resolve a JSON Schema $ref.""" if not ref.startswith("#"): raise ReferenceError("Only local references (#/...) are supported") if ref in self._processing_refs: raise ReferenceError(f"Circular reference detected: {ref}") self._processing_refs.add(ref) try: # Split the reference path and navigate through the schema path = ref.split("/")[1:] # Remove the '#' and split current = root_schema # Navigate through the schema for part in path: # Handle JSON Pointer escaping part = part.replace("~1", "/").replace("~0", "~") try: current = current[part] except KeyError: raise ReferenceError(f"Invalid reference path: {ref}") # If we find another reference, resolve it if isinstance(current, dict) and "$ref" in current: current = self.resolve_ref(current["$ref"], current, root_schema) return current finally: self._processing_refs.remove(ref) json-schema-to-pydantic-0.4.9/tests/000077500000000000000000000000001513773571500173445ustar00rootroot00000000000000json-schema-to-pydantic-0.4.9/tests/__init__.py000066400000000000000000000000001513773571500214430ustar00rootroot00000000000000json-schema-to-pydantic-0.4.9/tests/test_builders.py000066400000000000000000000105231513773571500225670ustar00rootroot00000000000000from datetime import datetime from typing import Literal from uuid import UUID from pydantic import AnyUrl from json_schema_to_pydantic.builders import ConstraintBuilder def test_string_constraints(): builder = ConstraintBuilder() constraints = builder.build_constraints( {"minLength": 3, "maxLength": 10, "pattern": "^[A-Z].*$"} ) assert constraints["min_length"] == 3 assert constraints["max_length"] == 10 assert constraints["pattern"] == "^[A-Z].*$" def test_format_constraints(): builder = ConstraintBuilder() email_constraints = builder.build_constraints({"format": "email"}) assert isinstance(email_constraints, dict) assert "pattern" in email_constraints assert ( email_constraints["pattern"] == r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" ) assert builder.build_constraints({"format": "date-time"}) == datetime assert builder.build_constraints({"format": "uuid"}) == UUID def test_numeric_constraints(): builder = ConstraintBuilder() constraints = builder.build_constraints( {"minimum": 0, "maximum": 100, "multipleOf": 5} ) assert constraints["ge"] == 0 assert constraints["le"] == 100 assert constraints["multiple_of"] == 5 def test_array_constraints(): """Test array-specific constraints.""" builder = ConstraintBuilder() constraints = builder.build_constraints({"minItems": 1, "maxItems": 5}) assert constraints["min_length"] == 1 assert constraints["max_length"] == 5 def test_exclusive_numeric_constraints(): """Test exclusive minimum/maximum constraints.""" builder = ConstraintBuilder() constraints = builder.build_constraints( {"exclusiveMinimum": 0, "exclusiveMaximum": 100} ) assert constraints["gt"] == 0 assert constraints["lt"] == 100 def test_multiple_constraints(): """Test combining multiple types of constraints.""" builder = ConstraintBuilder() constraints = builder.build_constraints( {"minLength": 3, "maxLength": 10, "pattern": "^[A-Z].*$", "format": "email"} ) assert "pattern" in constraints assert constraints["pattern"] == r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" assert constraints["min_length"] == 3 assert constraints["max_length"] == 10 def test_empty_schema(): """Test handling of empty schema.""" builder = ConstraintBuilder() constraints = builder.build_constraints({}) assert constraints == {} def test_const_constraint(): """Test handling of const constraint.""" builder = ConstraintBuilder() constraints = builder.build_constraints({"const": "dog"}) assert constraints == Literal["dog"] def test_uri_format(): """Test URI format handling.""" builder = ConstraintBuilder() constraints = builder.build_constraints({"format": "uri"}) assert constraints == AnyUrl def test_merge_numeric_constraints(): """Test merging numeric constraints.""" builder = ConstraintBuilder() schema1 = {"minimum": 0, "maximum": 100} schema2 = {"minimum": 10, "maximum": 50} merged = builder.merge_constraints(schema1, schema2) assert merged["minimum"] == 10 # Takes the more restrictive minimum assert merged["maximum"] == 50 # Takes the more restrictive maximum def test_merge_string_constraints(): """Test merging string constraints.""" builder = ConstraintBuilder() schema1 = {"minLength": 3, "maxLength": 10, "pattern": "^[A-Z]"} schema2 = {"minLength": 5, "maxLength": 8, "pattern": "[0-9]$"} merged = builder.merge_constraints(schema1, schema2) assert merged["minLength"] == 5 assert merged["maxLength"] == 8 assert "(?=^[A-Z])(?=[0-9]$)" in merged["pattern"] def test_merge_mixed_constraints(): """Test merging mixed constraint types.""" builder = ConstraintBuilder() schema1 = {"minimum": 0, "minLength": 3} schema2 = {"maximum": 100, "maxLength": 10} merged = builder.merge_constraints(schema1, schema2) assert merged["minimum"] == 0 assert merged["maximum"] == 100 assert merged["minLength"] == 3 assert merged["maxLength"] == 10 def test_merge_with_empty_schema(): """Test merging with an empty schema.""" builder = ConstraintBuilder() schema1 = {"minimum": 0} schema2 = {} merged = builder.merge_constraints(schema1, schema2) assert merged["minimum"] == 0 json-schema-to-pydantic-0.4.9/tests/test_handlers.py000066400000000000000000000666471513773571500226000ustar00rootroot00000000000000from typing import Union import pytest from pydantic import BaseModel, ConfigDict, Field, RootModel, create_model from json_schema_to_pydantic import PydanticModelBuilder from json_schema_to_pydantic.builders import ConstraintBuilder from json_schema_to_pydantic.exceptions import CombinerError from json_schema_to_pydantic.handlers import CombinerHandler from json_schema_to_pydantic.resolvers import ReferenceResolver, TypeResolver # Helper function to create handler with dependencies def create_handler(): # Instantiate necessary components type_resolver = TypeResolver() constraint_builder = ConstraintBuilder() reference_resolver = ReferenceResolver() # Define simple callbacks for testing purposes # Note: These might need adjustment if tests require full model building recursion def simple_recursive_builder( schema, root_schema, allow_undefined_array_items=False, allow_any_type=False, populate_by_name=False, ): # Resolve $ref first, similar to PydanticModelBuilder._get_field_type if "$ref" in schema: schema = reference_resolver.resolve_ref(schema["$ref"], schema, root_schema) # Basic type resolution for testing handlers if schema.get("type") == "object": # Simulate basic object model creation for tests needing nested structures props = schema.get("properties", {}) req = schema.get("required", []) fields = { n: ( simple_recursive_builder( p, root_schema, allow_undefined_array_items ), simple_field_info_builder(p, n in req), ) for n, p in props.items() } return create_model( "NestedTestModel", __config__=ConfigDict(populate_by_name=populate_by_name), **fields, ) elif "oneOf" in schema: # Delegate back to a temporary handler instance for nested oneOf # This is a bit complex for a simple test setup, might need refinement temp_handler = CombinerHandler( type_resolver, constraint_builder, reference_resolver, simple_recursive_builder, simple_field_info_builder, PydanticModelBuilder._sanitize_field_name, ) return temp_handler.handle_one_of( schema["oneOf"], root_schema, allow_undefined_array_items, allow_any_type, populate_by_name, ) elif "anyOf" in schema: temp_handler = CombinerHandler( type_resolver, constraint_builder, reference_resolver, simple_recursive_builder, simple_field_info_builder, ) return temp_handler.handle_any_of( schema["anyOf"], root_schema, allow_undefined_array_items, allow_any_type, populate_by_name, ) # Fallback to basic type resolver return type_resolver.resolve_type( schema, root_schema, allow_undefined_array_items, allow_any_type, populate_by_name ) def simple_field_info_builder(schema, required, alias=None): # Basic field info creation for testing kwargs = {} constraints = constraint_builder.build_constraints(schema) if isinstance(constraints, dict): kwargs.update(constraints) if "default" in schema: kwargs["default"] = schema["default"] elif not required: kwargs["default"] = None if "description" in schema: kwargs["description"] = schema["description"] if alias: kwargs["alias"] = alias return Field(**kwargs) # Return the handler instance with dependencies and callbacks return CombinerHandler( type_resolver=type_resolver, constraint_builder=constraint_builder, reference_resolver=reference_resolver, recursive_field_builder=simple_recursive_builder, field_info_builder=simple_field_info_builder, name_sanitizer=PydanticModelBuilder._sanitize_field_name, ) def test_all_of_handler(): handler = create_handler() schemas = [ { "type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"], }, { "type": "object", "properties": {"age": {"type": "integer"}}, "required": ["age"], }, ] model = handler.handle_all_of(schemas, {}) assert issubclass(model, BaseModel) # Validate merged model instance = model(name="John", age=30) assert instance.name == "John" assert instance.age == 30 def test_any_of_handler(): handler = create_handler() schemas = [{"type": "string"}, {"type": "integer"}] union_type = handler.handle_any_of(schemas, {}) assert Union[str, int] == union_type def test_one_of_handler(): handler = create_handler() schema = { "oneOf": [ { "type": "object", "properties": {"type": {"const": "dog"}, "bark": {"type": "boolean"}}, }, { "type": "object", "properties": {"type": {"const": "cat"}, "meow": {"type": "boolean"}}, }, ] } model = handler.handle_one_of(schema["oneOf"], {}) assert issubclass(model, BaseModel) def test_all_of_with_conflicting_constraints(): handler = create_handler() schemas = [ { "type": "object", "properties": {"value": {"type": "integer", "minimum": 0, "maximum": 100}}, }, { "type": "object", "properties": {"value": {"type": "integer", "minimum": 50, "maximum": 75}}, }, ] model = handler.handle_all_of(schemas, {}) instance = model(value=60) assert instance.value == 60 with pytest.raises(ValueError): model(value=25) # Below merged minimum with pytest.raises(ValueError): model(value=80) # Above merged maximum def test_any_of_with_mixed_types(): handler = create_handler() schemas = [ {"type": "string", "minLength": 3}, {"type": "integer", "minimum": 0}, {"type": "object", "properties": {"name": {"type": "string"}}}, ] union_type = handler.handle_any_of(schemas, {}) # Verify the union includes all types assert str in union_type.__args__ assert int in union_type.__args__ assert any(issubclass(t, BaseModel) for t in union_type.__args__) def test_one_of_validation(): handler = create_handler() schema = { "oneOf": [ { "type": "object", "properties": { "type": {"const": "circle"}, "radius": {"type": "number"}, }, "required": ["type", "radius"], }, { "type": "object", "properties": { "type": {"const": "rectangle"}, "width": {"type": "number"}, "height": {"type": "number"}, }, "required": ["type", "width", "height"], }, ] } model = handler.handle_one_of(schema["oneOf"], {}) # Test circle circle = model(type="circle", radius=5.0) assert circle.root.type == "circle" assert circle.root.radius == 5.0 # Test rectangle rectangle = model(type="rectangle", width=10.0, height=20.0) assert rectangle.root.type == "rectangle" assert rectangle.root.width == 10.0 assert rectangle.root.height == 20.0 # Test invalid type with pytest.raises(ValueError): model(type="triangle", sides=3) def test_empty_combiners(): handler = create_handler() with pytest.raises(CombinerError): handler.handle_all_of([], {}) with pytest.raises(CombinerError): handler.handle_any_of([], {}) with pytest.raises(CombinerError): handler.handle_one_of([], {}) def test_one_of_invalid_schema(): handler = create_handler() # Test with non-dict schema invalid_schema = {"oneOf": ["not a valid schema", {"type": "object"}]} with pytest.raises(CombinerError): handler.handle_one_of(invalid_schema, {}) def test_one_of_missing_discriminator(): handler = create_handler() # Schema missing type const invalid_schema = { "oneOf": [{"type": "object", "properties": {"name": {"type": "string"}}}] } with pytest.raises(CombinerError): handler.handle_one_of(invalid_schema, {}) def test_one_of_nested(): handler = create_handler() schema = { "oneOf": [ { "type": "object", "properties": { "type": {"const": "parent"}, "child": { "oneOf": [ { "type": "object", "properties": { "type": {"const": "child1"}, "value": {"type": "string"}, }, }, { "type": "object", "properties": { "type": {"const": "child2"}, "value": {"type": "integer"}, }, }, ] }, }, } ] } model = handler.handle_one_of(schema["oneOf"], {}) # Test nested discriminated union instance = model(type="parent", child={"type": "child1", "value": "test"}) assert instance.root.type == "parent" assert instance.root.child.root.type == "child1" assert instance.root.child.root.value == "test" assert instance.root.child.root.type == "child1" assert instance.root.child.root.value == "test" def test_one_of_required_fields(): handler = create_handler() schema = { "oneOf": [ { "type": "object", "properties": { "type": {"const": "user"}, "username": {"type": "string"}, }, "required": ["type", "username"], } ] } model = handler.handle_one_of(schema["oneOf"], {}) # Test missing required field with pytest.raises(ValueError): model(root={"type": "user"}) # Missing required username def test_optional_fields(): handler = create_handler() schema = { "oneOf": [ { "type": "object", "properties": { "type": {"const": "user"}, "username": {"type": "string"}, "email": {"type": "string", "format": "email"}, "description": {"type": "string"}, }, "required": ["type", "username"], # email and description are optional } ] } model = handler.handle_one_of(schema["oneOf"], {}) # Test with only required fields instance = model(root={"type": "user", "username": "test"}) assert instance.root.type == "user" assert instance.root.username == "test" assert instance.root.email is None assert instance.root.description is None def test_field_descriptions(): handler = create_handler() schema = { "oneOf": [ { "type": "object", "properties": { "type": {"const": "user", "description": "The type of user"}, "username": { "type": "string", "description": "The user's username", }, }, "required": ["type", "username"], } ] } model = handler.handle_one_of(schema["oneOf"], {}) # Get field info from model field_info = model.model_fields["root"].annotation.model_fields assert field_info["type"].description == "The type of user" assert field_info["username"].description == "The user's username" def test_one_of_invalid_discriminator(): handler = create_handler() schema = { "oneOf": [ { "type": "object", "properties": { "type": {"const": "option1"}, "value": {"type": "string"}, }, } ] } model = handler.handle_one_of(schema["oneOf"], {}) # Test invalid discriminator value with pytest.raises(ValueError): model(type="invalid_option", value="test") def test_any_of_with_ref(): """Test anyOf handler with a $ref.""" handler = create_handler() root_schema = { "$defs": { "Address": { "type": "object", "properties": {"street": {"type": "string"}}, "required": ["street"], } } } schemas = [{"type": "string"}, {"$ref": "#/$defs/Address"}] union_type = handler.handle_any_of(schemas, root_schema) # Check that the Union includes str and a BaseModel derived from Address assert str in union_type.__args__ assert any( issubclass(t, BaseModel) and "street" in t.model_fields for t in union_type.__args__ ) def test_one_of_with_ref(): """Test oneOf handler with a $ref.""" handler = create_handler() root_schema = { "$defs": { "Cat": { "type": "object", "properties": { "type": {"const": "cat", "description": "Type discriminator"}, "meow_volume": {"type": "integer"}, }, "required": ["type", "meow_volume"], } } } schema = { "oneOf": [ { "type": "object", "properties": { "type": {"const": "dog"}, "bark_pitch": {"type": "string"}, }, "required": ["type", "bark_pitch"], }, {"$ref": "#/$defs/Cat"}, ] } model = handler.handle_one_of(schema["oneOf"], root_schema) assert issubclass(model, RootModel) # Test dog variant dog_instance = model(root={"type": "dog", "bark_pitch": "high"}) assert dog_instance.root.type == "dog" assert dog_instance.root.bark_pitch == "high" # Test cat variant (resolved from $ref) cat_instance = model(root={"type": "cat", "meow_volume": 10}) assert cat_instance.root.type == "cat" assert cat_instance.root.meow_volume == 10 # Check field descriptions from referenced schema # Access the Union arguments inside Annotated: annotation.__args__[0].__args__ union_args = model.model_fields["root"].annotation.__args__[0].__args__ cat_model = next(t for t in union_args if t.__name__ == "Cat") assert cat_model.model_fields["type"].description == "Type discriminator" def test_any_of_property_with_ref(): """Test a property using anyOf containing a $ref.""" handler = create_handler() root_schema = { "$defs": { "SimpleObject": { "type": "object", "properties": {"id": {"type": "integer"}}, } }, "type": "object", "properties": { "data": { "anyOf": [ {"type": "string"}, {"$ref": "#/$defs/SimpleObject"}, ] } }, } # We need to simulate how ModelBuilder calls the handler for a property prop_schema = root_schema["properties"]["data"] union_type = handler.handle_any_of(prop_schema["anyOf"], root_schema) assert str in union_type.__args__ assert any( issubclass(t, BaseModel) and "id" in t.model_fields for t in union_type.__args__ ) def test_one_of_property_with_ref(): """Test a property using oneOf containing a $ref.""" handler = create_handler() root_schema = { "$defs": { "RefOption": { "type": "object", "properties": { "type": {"const": "ref_opt"}, "value": {"type": "boolean"}, }, "required": ["type", "value"], } }, "type": "object", "properties": { "choice": { "oneOf": [ { "type": "object", "properties": { "type": {"const": "inline_opt"}, "value": {"type": "string"}, }, "required": ["type", "value"], }, {"$ref": "#/$defs/RefOption"}, ] } }, } # Simulate ModelBuilder call for the property prop_schema = root_schema["properties"]["choice"] model = handler.handle_one_of(prop_schema["oneOf"], root_schema) assert issubclass(model, RootModel) # Test inline variant inline_instance = model(root={"type": "inline_opt", "value": "hello"}) assert inline_instance.root.type == "inline_opt" assert inline_instance.root.value == "hello" # Test referenced variant ref_instance = model(root={"type": "ref_opt", "value": True}) assert ref_instance.root.type == "ref_opt" assert ref_instance.root.value is True def test_all_of_with_top_level_ref(): """Test allOf handler with a top-level $ref in the list.""" handler = create_handler() root_schema = { "$defs": { "NameSchema": { "type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"], } } } schemas = [ {"$ref": "#/$defs/NameSchema"}, { "type": "object", "properties": {"age": {"type": "integer"}}, "required": ["age"], }, ] model = handler.handle_all_of(schemas, root_schema) assert issubclass(model, BaseModel) # Validate merged model includes fields from the referenced schema instance = model(name="Alice", age=30) assert instance.name == "Alice" assert instance.age == 30 # Check required fields from both schemas with pytest.raises(ValueError): model(name="Alice") # Missing age with pytest.raises(ValueError): model(age=30) # Missing name def test_all_of_with_property_ref(): """Test allOf handler where a property uses $ref.""" handler = create_handler() root_schema = { "$defs": { "Address": { "type": "object", "properties": { "street": {"type": "string"}, "city": {"type": "string"}, }, "required": ["street", "city"], } } } schemas = [ { "type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"], }, { "type": "object", "properties": {"location": {"$ref": "#/$defs/Address"}}, "required": ["location"], }, ] model = handler.handle_all_of(schemas, root_schema) assert issubclass(model, BaseModel) # Validate merged model includes the referenced property type instance = model(name="Bob", location={"street": "123 Main St", "city": "Anytown"}) assert instance.name == "Bob" assert isinstance(instance.location, BaseModel) assert instance.location.street == "123 Main St" assert instance.location.city == "Anytown" # Check required fields with pytest.raises(ValueError): model(name="Bob") # Missing location with pytest.raises(ValueError): model(location={"street": "123 Main St", "city": "Anytown"}) # Missing name with pytest.raises(ValueError): # Missing required field within the referenced Address model model(name="Bob", location={"street": "123 Main St"}) def test_all_of_merging_ref_and_inline(): """Test merging a schema defined by $ref with an inline schema.""" handler = create_handler() root_schema = { "$defs": { "BaseInfo": { "type": "object", "properties": { "id": {"type": "integer"}, "description": {"type": "string"}, }, "required": ["id"], } } } schemas = [ {"$ref": "#/$defs/BaseInfo"}, { "type": "object", "properties": { "description": {"minLength": 10}, # Add constraint to existing field "status": {"type": "string", "enum": ["active", "inactive"]}, }, "required": ["status"], # Add new required field }, ] model = handler.handle_all_of(schemas, root_schema) assert issubclass(model, BaseModel) # Validate merged model instance = model(id=1, description="A long description", status="active") assert instance.id == 1 assert instance.description == "A long description" assert instance.status == "active" # Check merged constraints and required fields with pytest.raises(ValueError): model(id=1, description="short", status="active") # Fails minLength with pytest.raises(ValueError): model(id=1, description="A long description") # Missing status with pytest.raises(ValueError): model(description="A long description", status="active") # Missing id def test_one_of_simple_type_union(): """Test oneOf with simple type variants (Issue #37).""" handler = create_handler() schemas = [ {"type": "integer"}, {"type": "string"}, ] union_type = handler.handle_one_of(schemas, {}) # Should return a Union type, not a discriminated model assert hasattr(union_type, "__args__") assert int in union_type.__args__ assert str in union_type.__args__ def test_one_of_const_union(): """Test oneOf with const values creates a Literal type.""" handler = create_handler() schemas = [ {"const": "red"}, {"const": "green"}, {"const": "blue"}, ] literal_type = handler.handle_one_of(schemas, {}) # Should return a Literal type from typing import get_args assert get_args(literal_type) == ("red", "green", "blue") def test_one_of_const_union_with_falsy_values(): """Test oneOf with falsy const values (empty string, 0, null).""" handler = create_handler() schemas = [ {"const": ""}, {"const": 0}, {"const": None}, {"const": False}, ] literal_type = handler.handle_one_of(schemas, {}) from typing import get_args assert get_args(literal_type) == ("", 0, None, False) def test_one_of_ref_without_discriminator(): """Test oneOf with $ref variants that don't have a type const discriminator.""" handler = create_handler() root_schema = { "$defs": { "StringOption": { "type": "object", "properties": {"value": {"type": "string"}}, }, "IntOption": { "type": "object", "properties": {"value": {"type": "integer"}}, }, } } schemas = [ {"$ref": "#/$defs/StringOption"}, {"$ref": "#/$defs/IntOption"}, ] # Should handle refs without discriminator by falling back to general union union_type = handler.handle_one_of(schemas, root_schema) assert hasattr(union_type, "__args__") def test_one_of_mixed_types(): """Test oneOf with a mix of simple types and objects.""" handler = create_handler() schemas = [ {"type": "string"}, {"type": "null"}, {"type": "object", "properties": {"name": {"type": "string"}}}, ] union_type = handler.handle_one_of(schemas, {}) assert hasattr(union_type, "__args__") # Should include str and None (for null) plus an object model assert str in union_type.__args__ assert type(None) in union_type.__args__ def test_one_of_const_with_mixed_valid_types(): """Test oneOf with const values of different valid Literal types. Literal supports str, int, bool, bytes, None. """ handler = create_handler() schemas = [ {"const": "string_value"}, {"const": 42}, {"const": True}, {"const": None}, ] literal_type = handler.handle_one_of(schemas, {}) from typing import get_args assert get_args(literal_type) == ("string_value", 42, True, None) def test_one_of_populate_by_name(): """Test oneOf handler with populate_by_name=True.""" handler = create_handler() schema = { "oneOf": [ { "type": "object", "properties": { "type": {"const": "user"}, "username": {"type": "string", "alias": "user_name"}, }, "required": ["type", "username"], }, { "type": "object", "properties": { "type": {"const": "admin"}, "admin_level": {"type": "integer", "alias": "adminLevel"}, }, "required": ["type", "admin_level"], }, ] } model = handler.handle_one_of( schema["oneOf"], {}, populate_by_name=True, ) from typing import get_args types = get_args(model.model_fields["root"].annotation)[0].__args__ assert len(types) == 2 assert types[0].model_config["populate_by_name"] is True assert types[1].model_config["populate_by_name"] is True def test_all_of_populate_by_name(): """Test allOf handler with populate_by_name=True.""" handler = create_handler() schemas = [ { "type": "object", "properties": { "first_name": {"type": "string", "alias": "firstName"}, "last_name": {"type": "string", "alias": "lastName"}, }, "required": ["first_name", "last_name"], }, { "type": "object", "properties": { "age": {"type": "integer", "alias": "userAge"}, }, "required": ["age"], }, ] model = handler.handle_all_of( schemas, {}, populate_by_name=True, ) assert model.model_config["populate_by_name"] is True def test_any_of_populate_by_name(): """Test anyOf handler with populate_by_name=True.""" handler = create_handler() schemas = [ { "type": "object", "properties": { "email_address": {"type": "string", "alias": "emailAddress"}, }, }, { "type": "object", "properties": { "phone_number": {"type": "string", "alias": "phoneNumber"}, }, }, ] union_type = handler.handle_any_of( schemas, {}, populate_by_name=True, ) from typing import get_args types = get_args(union_type) assert len(types) == 2 assert types[0].model_config["populate_by_name"] is True assert types[1].model_config["populate_by_name"] is True json-schema-to-pydantic-0.4.9/tests/test_model_builder.py000066400000000000000000000465261513773571500236000ustar00rootroot00000000000000import pytest from pydantic import BaseModel, Field, ValidationError # Explicitly import custom TypeError with an alias from json_schema_to_pydantic.exceptions import SchemaError, TypeError as JsonSchemaTypeError from json_schema_to_pydantic.model_builder import PydanticModelBuilder class CustomBaseModel(BaseModel): test_case: str = Field(default="test", description="A test case") def test_basic_model_creation(): """Test basic model creation with simple properties.""" builder = PydanticModelBuilder(base_model_type=CustomBaseModel) schema = { "title": "TestModel", "description": "A test model", "type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, "required": ["name"], } model = builder.create_pydantic_model(schema, CustomBaseModel) assert model.__name__ == "TestModel" assert model.__doc__ == "A test model" assert issubclass(model, CustomBaseModel) # Test instance creation instance = model(name="test", age=25) assert instance.name == "test" assert instance.age == 25 # Test required field validation with pytest.raises(ValueError): model(age=25) def test_model_builder_constructor_with_undefined_arrays(): """Test PydanticModelBuilder constructor with allow_undefined_array_items parameter.""" builder = PydanticModelBuilder() schema = { "type": "object", "properties": { "data": {"type": "array"} # No items defined }, } # Should work without explicitly passing allow_undefined_array_items to create_pydantic_model model = builder.create_pydantic_model(schema, allow_undefined_array_items=True) instance = model(data=[1, 2, 3]) assert instance.data == [1, 2, 3] def test_nested_model_creation(): """Test creation of models with nested objects.""" builder = PydanticModelBuilder() schema = { "type": "object", "properties": { "user": { "type": "object", "properties": { "name": {"type": "string"}, "address": { "type": "object", "properties": {"street": {"type": "string"}}, }, }, } }, } model = builder.create_pydantic_model(schema) instance = model(user={"name": "John", "address": {"street": "Main St"}}) assert isinstance(instance.user, BaseModel) assert isinstance(instance.user.address, BaseModel) assert instance.user.name == "John" assert instance.user.address.street == "Main St" def test_nested_undefined_array_items(): """Test handling of nested objects with undefined array items.""" builder = PydanticModelBuilder() schema = { "type": "object", "properties": { "user": { "type": "object", "properties": { "name": {"type": "string"}, "tags": {"type": "array"}, # No items defined }, } }, } # Should raise error with default settings # Use the explicit alias in pytest.raises with pytest.raises(JsonSchemaTypeError): builder.create_pydantic_model(schema) # Should work with allow_undefined_array_items=True model = builder.create_pydantic_model(schema, allow_undefined_array_items=True) instance = model(user={"name": "John", "tags": ["admin", "user"]}) assert instance.user.name == "John" assert instance.user.tags == ["admin", "user"] def test_model_with_references(): """Test model creation with schema references.""" builder = PydanticModelBuilder() schema = { "type": "object", "properties": {"current_pet": {"$ref": "#/definitions/Pet"}}, "definitions": { "Pet": { "type": "object", "properties": {"name": {"type": "string"}, "type": {"type": "string"}}, } }, } model = builder.create_pydantic_model(schema) instance = model(current_pet={"name": "Fluffy", "type": "cat"}) assert isinstance(instance.current_pet, BaseModel) assert instance.current_pet.name == "Fluffy" def test_model_with_combiners(): """Test model creation with schema combiners.""" builder = PydanticModelBuilder() schema = { "type": "object", "properties": { "mixed_field": { "oneOf": [ { "type": "object", "properties": { "type": {"const": "a"}, "value": {"type": "string"}, }, }, { "type": "object", "properties": { "type": {"const": "b"}, "value": {"type": "integer"}, }, }, ] } }, } model = builder.create_pydantic_model(schema) # Test both variants instance1 = model(mixed_field={"type": "a", "value": "test"}) instance2 = model(mixed_field={"type": "b", "value": 42}) assert instance1.mixed_field.root.type == "a" assert instance1.mixed_field.root.value == "test" assert instance2.mixed_field.root.type == "b" assert instance2.mixed_field.root.value == 42 def test_complex_schema_with_undefined_arrays(): """Test handling of complex schemas with multiple undefined arrays.""" builder = PydanticModelBuilder() schema = { "type": "object", "properties": { "name": {"type": "string"}, "tags": {"type": "array"}, # No items defined "metadata": { "type": "object", "properties": { "categories": {"type": "array"}, # No items defined "flags": {"type": "array"}, # No items defined }, }, "history": { "type": "array", # Array of objects with undefined arrays "items": { "type": "object", "properties": { "timestamp": {"type": "string"}, "actions": {"type": "array"}, # No items defined }, }, }, }, } # Should work with allow_undefined_array_items=True model = builder.create_pydantic_model(schema, allow_undefined_array_items=True) # Create a complex instance instance = model( name="Test", tags=["important", "urgent"], metadata={"categories": ["A", "B", "C"], "flags": [True, False, True]}, history=[ {"timestamp": "2023-01-01", "actions": ["created", "modified"]}, { "timestamp": "2023-01-02", "actions": ["reviewed", 123, {"status": "approved"}], }, ], ) # Verify all data is correctly stored assert instance.name == "Test" assert instance.tags == ["important", "urgent"] assert instance.metadata.categories == ["A", "B", "C"] assert instance.metadata.flags == [True, False, True] # Access history items as Pydantic models assert instance.history[0].timestamp == "2023-01-01" assert instance.history[0].actions == ["created", "modified"] assert instance.history[1].actions == ["reviewed", 123, {"status": "approved"}] def test_root_level_features(): """Test handling of root level schema features.""" builder = PydanticModelBuilder() schema = { "title": "CustomModel", "description": "A model with root level features", "type": "object", "properties": {"field": {"type": "string"}}, "$defs": { # Test both $defs and definitions "SubType": { "type": "object", "properties": {"subfield": {"type": "string"}}, } }, } model = builder.create_pydantic_model(schema) assert model.__name__ == "CustomModel" assert model.__doc__ == "A model with root level features" # Test that $defs are properly handled when referenced schema_with_ref = { "type": "object", "properties": {"sub": {"$ref": "#/$defs/SubType"}}, } model_with_ref = builder.create_pydantic_model(schema_with_ref, schema) instance = model_with_ref(sub={"subfield": "test"}) assert instance.sub.subfield == "test" def test_undefined_array_items(): """Test handling of arrays without defined item types.""" # Test with default behavior (should raise error) builder = PydanticModelBuilder() schema = {"type": "object", "properties": {"tags": {"type": "array"}}} # Use the explicit alias in pytest.raises with pytest.raises( JsonSchemaTypeError, match="Array type must specify 'items' schema" ): builder.create_pydantic_model(schema) # Test with allow_undefined_array_items=True model = builder.create_pydantic_model(schema, allow_undefined_array_items=True) # Should create a model with List[Any] field instance = model(tags=["tag1", "tag2"]) assert instance.tags == ["tag1", "tag2"] # Should accept any type of items instance = model(tags=[1, "two", 3.0, True]) assert instance.tags == [1, "two", 3.0, True] def test_undefined_type(): """Test handling of schemas without an explicit type.""" # Test with default behavior (should raise error) builder = PydanticModelBuilder() schema = {"type": "object", "properties": {"metadata": {"description": "Any metadata"}}} # Use the explicit alias in pytest.raises with pytest.raises( JsonSchemaTypeError, match="Schema must specify a type. Set allow_undefined_type=True" ): builder.create_pydantic_model(schema) # Test with allow_undefined_type=True model = builder.create_pydantic_model(schema, allow_undefined_type=True) # Should create a model with Any field instance = model(metadata={"key": "value"}) assert instance.metadata == {"key": "value"} # Should accept any type instance = model(metadata=[1, "two", 3.0, True]) assert instance.metadata == [1, "two", 3.0, True] def test_create_model_function(): """Test the create_model function from the package.""" from json_schema_to_pydantic import create_model # Test with undefined array items and allow_undefined_array_items=True schema = {"type": "object", "properties": {"tools": {"type": "array"}}} # Should raise error with default settings # Use the explicit alias in pytest.raises with pytest.raises(JsonSchemaTypeError): model = create_model(schema) # Should work with allow_undefined_array_items=True model = create_model(schema, allow_undefined_array_items=True) instance = model(tools=["hammer", "screwdriver"]) assert instance.tools == ["hammer", "screwdriver"] def test_json_schema_extra_support(): """Test json_schema_extra support for both models and fields.""" builder = PydanticModelBuilder() # Test field-level json_schema_extra field_schema = { "title": "FieldTestModel", "type": "object", "properties": { "field_with_extra": { "type": "string", "description": "A field with extra properties", "is_core_field": True, "custom_validation": "email", "ui_hint": "large_text" }, "normal_field": { "type": "integer", "description": "A normal field" } }, "required": ["field_with_extra"] } model = builder.create_pydantic_model(field_schema) # Check field with json_schema_extra field_info = model.model_fields["field_with_extra"] assert field_info.json_schema_extra == { "is_core_field": True, "custom_validation": "email", "ui_hint": "large_text" } assert field_info.description == "A field with extra properties" # Check normal field has no json_schema_extra normal_field_info = model.model_fields["normal_field"] assert normal_field_info.json_schema_extra is None assert normal_field_info.description == "A normal field" # Test model-level json_schema_extra model_schema = { "title": "ModelTestModel", "type": "object", "description": "A model with extra properties", "properties": { "field": {"type": "string"} }, "examples": [{"field": "example_value"}], "ui_config": {"theme": "dark"}, "version": "1.0.0" } model_with_extra = builder.create_pydantic_model(model_schema) generated_schema = model_with_extra.model_json_schema() # Check model-level json_schema_extra is preserved assert "examples" in generated_schema assert generated_schema["examples"] == [{"field": "example_value"}] assert "ui_config" in generated_schema assert generated_schema["ui_config"] == {"theme": "dark"} assert "version" in generated_schema assert generated_schema["version"] == "1.0.0" # Standard properties should still be present assert generated_schema["title"] == "ModelTestModel" assert generated_schema["type"] == "object" assert "properties" in generated_schema # Test roundtrip: create model from generated schema roundtrip_model = builder.create_pydantic_model(generated_schema) roundtrip_schema = roundtrip_model.model_json_schema() # Should be identical assert roundtrip_schema == generated_schema def test_json_schema_extra_with_user_example(): """Test the exact user example from the issue.""" from pydantic import BaseModel, Field, ConfigDict # Original model as user defined class A(BaseModel): v: int = Field(..., description="This is a field", is_core_field=True) model_config = ConfigDict(json_schema_extra={'examples': [{'a': 'Foo'}]}) # Get schema and reconstruct schema = A.model_json_schema() builder = PydanticModelBuilder() A_recon = builder.create_pydantic_model(schema) # Check that everything matches original_schema = A.model_json_schema() reconstructed_schema = A_recon.model_json_schema() assert original_schema == reconstructed_schema assert A.model_fields['v'].json_schema_extra == A_recon.model_fields['v'].json_schema_extra # Test model functionality instance = A_recon(v=42) assert instance.v == 42 @pytest.mark.parametrize("populate_by_name", [True, False]) def test_model_with_underscore_property(populate_by_name): """Test model creation with properties that start with an underscore.""" builder = PydanticModelBuilder(base_model_type=CustomBaseModel) schema = { "title": "TestModel", "description": "A test model", "type": "object", "properties": {"_name": {"type": "string"}, "age": {"type": "integer"}}, "required": ["_name"], } model = builder.create_pydantic_model(schema, populate_by_name=populate_by_name) assert model.__name__ == "TestModel" assert model.__doc__ == "A test model" assert issubclass(model, CustomBaseModel) # Test instance creation instance = model(_name="test", age=25) assert instance.name == "test" assert instance.age == 25 assert instance.model_dump(by_alias=True) == {"_name": "test", "age": 25, "test_case": "test"} if populate_by_name: instance = model(name="test2", age=30) assert instance.name == "test2" assert instance.age == 30 assert instance.model_dump(by_alias=True) == {"_name": "test2", "age": 30, "test_case": "test"} else: with pytest.raises(ValidationError, match="1 validation error for TestModel\n_name"): model(name="test", age=25) # Test required field validation with pytest.raises(ValueError): model(age=25) def test_nested_model_with_underscore_property(): """Test nested model creation with properties that start with an underscore.""" builder = PydanticModelBuilder() schema = { "type": "object", "properties": { "user": { "type": "object", "properties": { "_name": {"type": "string"}, "address": { "type": "object", "properties": {"_street": {"type": "string"}}, }, }, } }, } model = builder.create_pydantic_model(schema) instance = model(user={"_name": "John", "address": {"_street": "Main St"}}) assert isinstance(instance.user, BaseModel) assert isinstance(instance.user.address, BaseModel) assert instance.user.name == "John" assert instance.user.address.street == "Main St" def test_model_with_underscore_collision(): """Test model creation with properties that collide after removing underscore.""" builder = PydanticModelBuilder() schema = { "title": "CollisionModel", "description": "A model with colliding properties", "type": "object", "properties": {"_name": {"type": "string"}, "name": {"type": "string"}}, "required": ["_name", "name"], } with pytest.raises(SchemaError, match="Duplicate field name after sanitization: 'name'"): builder.create_pydantic_model(schema) def test_model_with_one_of_combiner_with_underscore_property(): """Test model creation with combiners and properties that start with an underscore.""" builder = PydanticModelBuilder() schema = { "type": "object", "properties": { "mixed_field": { "oneOf": [ { "type": "object", "properties": { "_type": {"const": "a"}, "value": {"type": "string"}, }, }, { "type": "object", "properties": { "_type": {"const": "b"}, "value": {"type": "integer"}, }, }, ] } }, } model = builder.create_pydantic_model(schema, populate_by_name=True) # Test both variants instance1 = model(mixed_field={"_type": "a", "value": "test"}) instance2 = model(mixed_field={"_type": "b", "value": 42}) assert instance1.mixed_field.type == "a" assert instance1.mixed_field.value == "test" assert instance2.mixed_field.type == "b" assert instance2.mixed_field.value == 42 def test_model_with_all_of_combiner_with_underscore_property(): """Test model creation with allOf combiners and properties that start with an underscore.""" builder = PydanticModelBuilder() schema = { "type": "object", "properties": { "combined_field": { "allOf": [ {"type": "object", "properties": {"_name": {"type": "string"}}}, {"type": "object", "properties": {"age": {"type": "integer"}}} ] } }, } model = builder.create_pydantic_model(schema, populate_by_name=True) instance = model(combined_field={"_name": "Alice", "age": 30}) assert instance.combined_field.name == "Alice" assert instance.combined_field.age == 30 json-schema-to-pydantic-0.4.9/tests/test_recursive_models.py000066400000000000000000000120631513773571500243310ustar00rootroot00000000000000"""Tests for recursive model handling.""" from json_schema_to_pydantic.model_builder import PydanticModelBuilder def test_recursive_model_with_array(): """Test creation of recursive models where objects reference themselves through arrays.""" builder = PydanticModelBuilder() schema = { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "Node": { "type": "object", "properties": { "value": { "type": "string" }, "children": { "type": "array", "items": { "$ref": "#/definitions/Node" } } }, "required": ["value"] } }, "type": "object", "properties": { "root": { "$ref": "#/definitions/Node" } }, "required": ["root"] } # Should not raise RecursionError model = builder.create_pydantic_model(schema) # Verify model was created assert model is not None assert "root" in model.model_fields # Test instantiation with nested structure instance = model(root={ "value": "root", "children": [ {"value": "child1", "children": [{"value": "grandchild1"}]}, {"value": "child2"} ] }) # Verify the structure assert instance.root.value == "root" assert len(instance.root.children) == 2 assert instance.root.children[0].value == "child1" assert len(instance.root.children[0].children) == 1 assert instance.root.children[0].children[0].value == "grandchild1" assert instance.root.children[1].value == "child2" assert instance.root.children[1].children is None def test_recursive_model_with_optional_reference(): """Test recursive models with optional self-reference.""" builder = PydanticModelBuilder() schema = { "definitions": { "LinkedNode": { "type": "object", "properties": { "data": {"type": "string"}, "next": {"$ref": "#/definitions/LinkedNode"} }, "required": ["data"] } }, "type": "object", "properties": { "head": {"$ref": "#/definitions/LinkedNode"} }, "required": ["head"] } model = builder.create_pydantic_model(schema) # Test with a linked list instance = model(head={ "data": "first", "next": { "data": "second", "next": { "data": "third" } } }) assert instance.head.data == "first" assert instance.head.next.data == "second" assert instance.head.next.next.data == "third" assert instance.head.next.next.next is None def test_recursive_model_title_from_definition(): """Test that recursive models get their title from the definition name.""" builder = PydanticModelBuilder() schema = { "definitions": { "TreeNode": { "type": "object", "properties": { "value": {"type": "integer"}, "left": {"$ref": "#/definitions/TreeNode"}, "right": {"$ref": "#/definitions/TreeNode"} }, "required": ["value"] } }, "$ref": "#/definitions/TreeNode" } model = builder.create_pydantic_model(schema) # The model should be named TreeNode, not DynamicModel assert model.__name__ == "TreeNode" # Test a binary tree structure instance = model( value=1, left={"value": 2, "left": {"value": 4}}, right={"value": 3} ) assert instance.value == 1 assert instance.left.value == 2 assert instance.left.left.value == 4 assert instance.right.value == 3 def test_multiple_recursive_definitions(): """Test schema with multiple mutually recursive definitions.""" builder = PydanticModelBuilder() schema = { "definitions": { "Person": { "type": "object", "properties": { "name": {"type": "string"}, "children": { "type": "array", "items": {"$ref": "#/definitions/Person"} }, "spouse": {"$ref": "#/definitions/Person"} }, "required": ["name"] } }, "$ref": "#/definitions/Person" } model = builder.create_pydantic_model(schema) # Test a family tree instance = model( name="Alice", children=[ {"name": "Bob"}, {"name": "Carol"} ], spouse={"name": "David"} ) assert instance.name == "Alice" assert len(instance.children) == 2 assert instance.children[0].name == "Bob" assert instance.spouse.name == "David" json-schema-to-pydantic-0.4.9/tests/test_resolvers.py000066400000000000000000000434441513773571500230120ustar00rootroot00000000000000import pytest # Explicitly import custom TypeError with an alias from json_schema_to_pydantic.exceptions import ( ReferenceError, ) from json_schema_to_pydantic.exceptions import ( TypeError as JsonSchemaTypeError, ) from json_schema_to_pydantic.resolvers import ReferenceResolver, TypeResolver def test_type_resolver_basic_types(): resolver = TypeResolver() assert resolver.resolve_type({"type": "string"}, {}) is str assert resolver.resolve_type({"type": "integer"}, {}) is int assert resolver.resolve_type({"type": "number"}, {}) is float assert resolver.resolve_type({"type": "boolean"}, {}) is bool def test_type_resolver_array(): resolver = TypeResolver() schema = {"type": "array", "items": {"type": "string"}} from typing import List assert resolver.resolve_type(schema, {}) == List[str] def test_type_resolver_optional_array(): resolver = TypeResolver() schema = {"type": ["array", "null"], "items": {"type": "string"}} from typing import List, Optional assert resolver.resolve_type(schema, {}) == Optional[List[str]] def test_type_resolver_undefined_array_items(): """Test handling of arrays without defined item types.""" resolver = TypeResolver() from typing import Any, List # Test with undefined items and allow_undefined_array_items=False (default) schema = {"type": "array"} # Use the explicit alias in pytest.raises with pytest.raises( JsonSchemaTypeError, match="Array type must specify 'items' schema" ): resolver.resolve_type(schema, {}) # Test with undefined items and allow_undefined_array_items=True result = resolver.resolve_type(schema, {}, allow_undefined_array_items=True) assert result == List[Any] def test_reference_resolver(): resolver = ReferenceResolver() root_schema = { "definitions": { "pet": {"type": "object", "properties": {"name": {"type": "string"}}} } } result = resolver.resolve_ref("#/definitions/pet", {}, root_schema) assert result["type"] == "object" assert "name" in result["properties"] def test_invalid_reference_path(): """Test handling of invalid reference paths.""" resolver = ReferenceResolver() schema = {"$ref": "#/invalid/path"} root_schema = {"valid": {"field": "value"}} with pytest.raises(ReferenceError, match="Invalid reference path"): resolver.resolve_ref("#/invalid/path", schema, root_schema) def test_external_reference_rejection(): """Test rejection of external references.""" resolver = ReferenceResolver() schema = {"$ref": "http://example.com/schema"} root_schema = {} with pytest.raises(ReferenceError, match="Only local references"): resolver.resolve_ref("http://example.com/schema", schema, root_schema) def test_nested_reference_resolution(): """Test resolution of nested references.""" resolver = ReferenceResolver() root_schema = { "definitions": { "address": {"type": "object", "properties": {"street": {"type": "string"}}}, "person": {"$ref": "#/definitions/address"}, } } result = resolver.resolve_ref("#/definitions/person", {}, root_schema) assert result == {"type": "object", "properties": {"street": {"type": "string"}}} def test_circular_reference_detection(): """Test detection of direct circular references (ref to itself).""" resolver = ReferenceResolver() root_schema = { "definitions": { # This is a direct circular reference where a definition references itself directly "person": {"$ref": "#/definitions/person"} } } with pytest.raises(ReferenceError, match="Circular reference detected"): resolver.resolve_ref("#/definitions/person", {}, root_schema) def test_json_pointer_escaping(): """Test handling of escaped JSON Pointer characters.""" resolver = ReferenceResolver() root_schema = {"definitions": {"special/~name": {"type": "string"}}} result = resolver.resolve_ref("#/definitions/special~1~0name", {}, root_schema) assert result == {"type": "string"} def test_type_resolver_complex_types(): """Test resolution of complex types like arrays and objects.""" resolver = TypeResolver() # Test nested array nested_array_schema = { "type": "array", "items": {"type": "array", "items": {"type": "string"}}, } from typing import List assert resolver.resolve_type(nested_array_schema, {}) == List[List[str]] # Test object with nested types object_schema = { "type": "object", "properties": { "strings": {"type": "array", "items": {"type": "string"}}, "numbers": {"type": "array", "items": {"type": "number"}}, }, } result_type = resolver.resolve_type(object_schema, {}) assert issubclass(result_type, dict) def test_type_resolver_format_handling(): """Test handling of format specifications.""" resolver = TypeResolver() from datetime import datetime, date, time from uuid import UUID from pydantic import AnyUrl assert ( resolver.resolve_type({"type": "string", "format": "date-time"}, {}) == datetime ) assert resolver.resolve_type({"type": "string", "format": "date"}, {}) == date assert resolver.resolve_type({"type": "string", "format": "time"}, {}) == time assert resolver.resolve_type({"type": "string", "format": "email"}, {}) is str assert resolver.resolve_type({"type": "string", "format": "uri"}, {}) == AnyUrl assert resolver.resolve_type({"type": "string", "format": "uuid"}, {}) == UUID def test_type_resolver_enum(): """Test handling of enum types.""" resolver = TypeResolver() from typing import Literal schema = {"type": "string", "enum": ["red", "green", "blue"]} result = resolver.resolve_type(schema, {}) assert result == Literal["red", "green", "blue"] def test_type_resolver_const(): """Test handling of const values.""" resolver = TypeResolver() from typing import Literal schema = {"const": 42} result = resolver.resolve_type(schema, {}) assert result == Literal[42] # Test null const schema = {"const": None} result = resolver.resolve_type(schema, {}) assert result is type(None) def test_type_resolver_null(): """Test handling of null type.""" resolver = TypeResolver() from typing import Optional schema = {"type": "null"} result = resolver.resolve_type(schema, {}) assert result is type(None) # Test nullable string schema = {"type": ["string", "null"]} result = resolver.resolve_type(schema, {}) assert result == Optional[str] def test_recursive_reference_not_detected_in_properties(): """Test that recursive references in properties are NOT detected as circular. This is intentional - the model builder handles recursive references properly using forward references, so the resolver should not eagerly resolve refs in properties. """ resolver = ReferenceResolver() root_schema = { "definitions": { "person": { "type": "object", "properties": {"friend": {"$ref": "#/definitions/person"}}, } } } # This should NOT raise an error - the reference is preserved for the model builder to handle result = resolver.resolve_ref("#/definitions/person", {}, root_schema) assert result["type"] == "object" assert "friend" in result["properties"] # The friend property should still have the $ref assert result["properties"]["friend"]["$ref"] == "#/definitions/person" def test_reference_resolver_nested_definitions(): """Test resolution of nested definitions.""" resolver = ReferenceResolver() root_schema = { "definitions": { "size": { "type": "object", "properties": { "width": {"type": "number"}, "height": {"type": "number"}, }, }, "shape": { "type": "object", "properties": { "name": {"type": "string"}, "size": {"$ref": "#/definitions/size"}, }, }, } } result = resolver.resolve_ref("#/definitions/shape", {}, root_schema) assert result["type"] == "object" assert "name" in result["properties"] # The size property should still have the $ref - it's not eagerly resolved assert result["properties"]["size"]["$ref"] == "#/definitions/size" def test_reference_resolver_array_refs(): """Test resolution of references in array items.""" resolver = ReferenceResolver() root_schema = { "definitions": { "point": { "type": "object", "properties": {"x": {"type": "number"}, "y": {"type": "number"}}, } } } schema = {"type": "array", "items": {"$ref": "#/definitions/point"}} result = resolver.resolve_ref("#/definitions/point", schema, root_schema) assert result["type"] == "object" assert "x" in result["properties"] assert "y" in result["properties"] def test_reference_resolver_invalid_path_segments(): """Test handling of invalid path segments in references.""" resolver = ReferenceResolver() root_schema = {"definitions": {"valid": {"type": "string"}}} with pytest.raises(ReferenceError, match="Invalid reference path"): resolver.resolve_ref("#/invalid/path/segments", {}, root_schema) def test_reference_resolver_empty_path(): """Test handling of empty reference paths.""" resolver = ReferenceResolver() with pytest.raises(ReferenceError): resolver.resolve_ref("", {}, {}) def test_reference_resolver_malformed_ref(): """Test handling of malformed references.""" resolver = ReferenceResolver() with pytest.raises(ReferenceError): resolver.resolve_ref("not/a/valid/ref", {}, {}) def test_type_resolver_nested_array_references(): """Test handling of nested references in array items.""" resolver = TypeResolver() # Create a schema with a reference in an array item root_schema = { "definitions": { "data": { "type": "object", "properties": {"b": {"type": "integer"}}, "required": ["b"], } }, "type": "object", "properties": {"a": {"type": "array", "items": {"$ref": "#/definitions/data"}}}, "required": ["a"], } # Test resolving the array type with nested reference array_schema = root_schema["properties"]["a"] result_type = resolver.resolve_type(array_schema, root_schema) from typing import get_args, get_origin # The result should be List[dict] assert get_origin(result_type) is list assert get_args(result_type)[0] is dict # Verify we can create and validate a model with this schema from json_schema_to_pydantic import create_model model = create_model(root_schema) instance = model.model_validate({"a": [{"b": 1}, {"b": 2}]}) # Access as Pydantic models assert instance.a[0].b == 1 assert instance.a[1].b == 2 def test_type_resolver_complex_nested_references(): """Test handling of complex nested references in array items.""" resolver = TypeResolver() # Create a more complex schema with nested references root_schema = { "definitions": { "nested_item": { "type": "object", "properties": { "name": {"type": "string"}, "value": {"type": "integer"}, }, "required": ["name", "value"], }, "complex_item": { "type": "object", "properties": { "id": {"type": "string"}, "nested_items": { "type": "array", "items": {"$ref": "#/definitions/nested_item"}, }, "metadata": { "type": ["object", "null"], "additionalProperties": {"type": "string"}, }, }, "required": ["id", "nested_items"], }, }, "type": "object", "properties": { "items": {"type": "array", "items": {"$ref": "#/definitions/complex_item"}}, "description": {"type": ["string", "null"]}, }, "required": ["items"], } # Test resolving the array type with complex nested references items_schema = root_schema["properties"]["items"] result_type = resolver.resolve_type(items_schema, root_schema) from typing import get_args, get_origin # The result should be List[dict] assert get_origin(result_type) is list assert get_args(result_type)[0] is dict # Verify we can create and validate a model with this schema from json_schema_to_pydantic import create_model model = create_model(root_schema) instance = model.model_validate( { "items": [ { "id": "item1", "nested_items": [ {"name": "nested1", "value": 10}, {"name": "nested2", "value": 20}, ], "metadata": {"key1": "value1", "key2": "value2"}, }, {"id": "item2", "nested_items": [{"name": "nested3", "value": 30}]}, ], "description": "A complex nested structure", } ) # Verify the structure was correctly parsed # Access as Pydantic models (our fix now creates proper models) assert instance.items[0].id == "item1" assert instance.items[0].nested_items[0].name == "nested1" assert instance.items[0].nested_items[0].value == 10 assert instance.items[0].metadata["key1"] == "value1" # metadata is still dict due to additionalProperties assert instance.items[1].id == "item2" assert instance.items[1].nested_items[0].name == "nested3" assert instance.items[1].nested_items[0].value == 30 assert instance.description == "A complex nested structure" def test_type_resolver_anytype(): """Test handling of anyType.""" resolver = TypeResolver() from typing import Any, Optional # Test "anyType" schema_any = {"type": "anyType"} result_any = resolver.resolve_type(schema_any, {}) assert result_any is Any # Test ["null", "anyType"] schema_optional_any = {"type": ["null", "anyType"]} result_optional_any = resolver.resolve_type(schema_optional_any, {}) assert result_optional_any == Optional[Any] # Test ["anyType", "null"] (order shouldn't matter) schema_optional_any_reversed = {"type": ["anyType", "null"]} result_optional_any_reversed = resolver.resolve_type( schema_optional_any_reversed, {} ) assert result_optional_any_reversed == Optional[Any] def test_type_resolver_multiple_types_without_null(): """Test handling of multiple types without null (Union).""" resolver = TypeResolver() from typing import Union # Test ["string", "integer"] schema = {"type": ["string", "integer"]} result = resolver.resolve_type(schema, {}) assert result == Union[str, int] # Test ["string", "number", "boolean"] schema = {"type": ["string", "number", "boolean"]} result = resolver.resolve_type(schema, {}) assert result == Union[str, float, bool] def test_type_resolver_multiple_types_with_null(): """Test handling of multiple types with null (Optional[Union]).""" resolver = TypeResolver() from typing import Optional, Union # Test ["string", "integer", "null"] schema = {"type": ["string", "integer", "null"]} result = resolver.resolve_type(schema, {}) assert result == Optional[Union[str, int]] # Test ["string", "number", "boolean", "null"] schema = {"type": ["string", "number", "boolean", "null"]} result = resolver.resolve_type(schema, {}) assert result == Optional[Union[str, float, bool]] # Test order shouldn't matter schema = {"type": ["null", "string", "integer"]} result = resolver.resolve_type(schema, {}) assert result == Optional[Union[str, int]] def test_type_resolver_multiple_complex_types(): """Test handling of multiple complex types.""" resolver = TypeResolver() from typing import List, Optional, Union # Test ["array", "string"] - array or string schema = {"type": ["array", "string"], "items": {"type": "integer"}} result = resolver.resolve_type(schema, {}) assert result == Union[List[int], str] # Test ["array", "string", "null"] - optional array or string schema = {"type": ["array", "string", "null"], "items": {"type": "integer"}} result = resolver.resolve_type(schema, {}) assert result == Optional[Union[List[int], str]] def test_type_resolver_edge_cases(): """Test edge cases with type arrays.""" resolver = TypeResolver() # Test single type in array (unusual but valid) schema = {"type": ["string"]} result = resolver.resolve_type(schema, {}) assert result is str # Test only null in array (unusual but valid) schema = {"type": ["null"]} result = resolver.resolve_type(schema, {}) assert result is type(None) def test_type_resolver_undefined_type(): """Test handling of schemas without an explicit type.""" resolver = TypeResolver() from typing import Any # Test with undefined type and allow_undefined_type=False (default) schema = {"description": "A field without a type"} # Use the explicit alias in pytest.raises with pytest.raises( JsonSchemaTypeError, match="Schema must specify a type. Set allow_undefined_type=True" ): resolver.resolve_type(schema, {}) # Test with undefined type and allow_undefined_type=True result = resolver.resolve_type(schema, {}, allow_undefined_type=True) assert result is Any json-schema-to-pydantic-0.4.9/tests/test_scalar_root.py000066400000000000000000000025131513773571500232660ustar00rootroot00000000000000from pydantic import RootModel, ValidationError import pytest from json_schema_to_pydantic.model_builder import PydanticModelBuilder def test_string_root_model(): schema = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "customers key", "type": "string", } builder = PydanticModelBuilder() model = builder.create_pydantic_model(schema) assert issubclass(model, RootModel) assert model.model_validate("test").root == "test" with pytest.raises(ValidationError): model.model_validate(123) def test_integer_root_model(): schema = {"title": "age", "type": "integer", "minimum": 0} builder = PydanticModelBuilder() model = builder.create_pydantic_model(schema) assert issubclass(model, RootModel) assert model.model_validate(10).root == 10 with pytest.raises(ValidationError): model.model_validate("test") with pytest.raises(ValidationError): model.model_validate(-1) def test_enum_root_model(): schema = {"title": "status", "enum": ["active", "inactive"]} builder = PydanticModelBuilder() model = builder.create_pydantic_model(schema) assert issubclass(model, RootModel) assert model.model_validate("active").root == "active" with pytest.raises(ValidationError): model.model_validate("pending") json-schema-to-pydantic-0.4.9/tests/test_top_level_array.py000066400000000000000000000254151513773571500241530ustar00rootroot00000000000000"""Tests for top-level array schemas.""" import pytest from pydantic import BaseModel, RootModel, ValidationError from json_schema_to_pydantic import create_model from json_schema_to_pydantic.exceptions import TypeError as JsonSchemaTypeError def test_basic_top_level_array(): """Test basic top-level array with object items.""" schema = { "type": "array", "items": { "type": "object", "properties": {"value": {"type": "number"}}, "required": ["value"], }, } data = [{"value": "42"}, {"value": "3.14"}] Model = create_model(schema) # Verify it's a RootModel assert issubclass(Model, RootModel) # Validate and check the data model = Model.model_validate(data) assert len(model.root) == 2 assert model.root[0].value == 42.0 assert model.root[1].value == 3.14 # Check JSON serialization json_output = model.model_dump_json() assert json_output == '[{"value":42.0},{"value":3.14}]' def test_top_level_array_with_simple_items(): """Test top-level array with simple string items.""" schema = { "type": "array", "items": {"type": "string"} } data = ["apple", "banana", "cherry"] Model = create_model(schema) model = Model.model_validate(data) assert model.root == ["apple", "banana", "cherry"] assert model.model_dump_json() == '["apple","banana","cherry"]' def test_top_level_array_with_number_items(): """Test top-level array with number items.""" schema = { "type": "array", "items": {"type": "number"} } data = [1, 2.5, 3.14] Model = create_model(schema) model = Model.model_validate(data) assert model.root == [1, 2.5, 3.14] def test_top_level_array_minItems_constraint(): """Test top-level array with minItems constraint.""" schema = { "type": "array", "items": {"type": "string"}, "minItems": 2 } Model = create_model(schema) # Should succeed with 2 or more items model = Model.model_validate(["a", "b"]) assert len(model.root) == 2 model = Model.model_validate(["a", "b", "c"]) assert len(model.root) == 3 # Should fail with less than 2 items with pytest.raises(ValidationError): Model.model_validate(["a"]) def test_top_level_array_maxItems_constraint(): """Test top-level array with maxItems constraint.""" schema = { "type": "array", "items": {"type": "string"}, "maxItems": 3 } Model = create_model(schema) # Should succeed with 3 or fewer items model = Model.model_validate(["a", "b", "c"]) assert len(model.root) == 3 # Should fail with more than 3 items with pytest.raises(ValidationError): Model.model_validate(["a", "b", "c", "d"]) def test_top_level_array_minItems_and_maxItems(): """Test top-level array with both minItems and maxItems constraints.""" schema = { "type": "array", "items": {"type": "integer"}, "minItems": 2, "maxItems": 5 } Model = create_model(schema) # Should succeed with 2-5 items for count in range(2, 6): data = list(range(count)) model = Model.model_validate(data) assert len(model.root) == count # Should fail with 1 item with pytest.raises(ValidationError): Model.model_validate([1]) # Should fail with 6 items with pytest.raises(ValidationError): Model.model_validate([1, 2, 3, 4, 5, 6]) def test_top_level_array_uniqueItems(): """Test top-level array with uniqueItems constraint.""" schema = { "type": "array", "items": {"type": "string"}, "uniqueItems": True } Model = create_model(schema) model = Model.model_validate(["a", "b", "c"]) # Should be a set assert isinstance(model.root, set) assert model.root == {"a", "b", "c"} # Duplicates should be automatically removed by set model = Model.model_validate(["a", "b", "a", "c"]) assert len(model.root) == 3 assert model.root == {"a", "b", "c"} def test_top_level_array_with_title(): """Test that title is properly used for top-level array models.""" schema = { "title": "MyArrayModel", "type": "array", "items": {"type": "string"} } Model = create_model(schema) assert Model.__name__ == "MyArrayModel" def test_top_level_array_with_description(): """Test that description is properly used for top-level array models.""" schema = { "type": "array", "description": "A list of items", "items": {"type": "string"} } Model = create_model(schema) assert Model.__doc__ == "A list of items" def test_top_level_array_nested_objects(): """Test top-level array with complex nested objects.""" schema = { "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer"}, "address": { "type": "object", "properties": { "street": {"type": "string"}, "city": {"type": "string"} } } }, "required": ["name"] } } data = [ { "name": "John", "age": 30, "address": {"street": "Main St", "city": "NYC"} }, { "name": "Jane", "age": 25 } ] Model = create_model(schema) model = Model.model_validate(data) assert len(model.root) == 2 assert model.root[0].name == "John" assert model.root[0].age == 30 assert model.root[0].address.street == "Main St" assert model.root[1].name == "Jane" assert model.root[1].age == 25 def test_top_level_array_without_items_raises_error(): """Test that array without items schema raises an error.""" schema = { "type": "array" # No items defined } with pytest.raises(JsonSchemaTypeError, match="Array type must specify 'items' schema"): create_model(schema) def test_top_level_array_without_items_with_flag(): """Test array without items schema works with allow_undefined_array_items flag.""" schema = { "type": "array" # No items defined } Model = create_model(schema, allow_undefined_array_items=True) # Should accept any items model = Model.model_validate([1, "two", 3.0, True, None]) assert len(model.root) == 5 def test_top_level_array_validation_errors(): """Test that validation errors work correctly for top-level arrays.""" schema = { "type": "array", "items": { "type": "object", "properties": { "value": {"type": "number"} }, "required": ["value"] } } Model = create_model(schema) # Should fail if required field is missing with pytest.raises(ValidationError) as exc_info: Model.model_validate([{"value": 42}, {}]) assert "value" in str(exc_info.value).lower() def test_top_level_array_with_ref_items(): """Test top-level array with items defined via $ref.""" schema = { "type": "array", "items": {"$ref": "#/$defs/Item"}, "$defs": { "Item": { "type": "object", "properties": { "id": {"type": "integer"}, "name": {"type": "string"} }, "required": ["id"] } } } data = [ {"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"} ] Model = create_model(schema) model = Model.model_validate(data) assert len(model.root) == 2 assert model.root[0].id == 1 assert model.root[0].name == "Item 1" assert model.root[1].id == 2 def test_array_as_property_still_works(): """Test that arrays as object properties still work (regression test).""" schema = { "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": {"value": {"type": "number"}}, "required": ["value"] } } } } data = {"items": [{"value": "42"}, {"value": "3.14"}]} Model = create_model(schema) # Should NOT be a RootModel assert not issubclass(Model, RootModel) assert issubclass(Model, BaseModel) model = Model.model_validate(data) assert len(model.items) == 2 assert model.items[0].value == 42.0 assert model.items[1].value == 3.14 def test_top_level_array_empty(): """Test that empty arrays are handled correctly.""" schema = { "type": "array", "items": {"type": "string"} } Model = create_model(schema) model = Model.model_validate([]) assert model.root == [] assert model.model_dump_json() == "[]" def test_top_level_array_with_custom_base_model(): """Test top-level array with custom base model type.""" class CustomBase(BaseModel): custom_field: str = "custom" schema = { "type": "array", "items": {"type": "string"} } # Note: RootModel doesn't inherit from custom base in the same way, # but we should still be able to create the model Model = create_model(schema, base_model_type=CustomBase) model = Model.model_validate(["a", "b", "c"]) assert model.root == ["a", "b", "c"] def test_top_level_array_extra_properties(): """Test that non-standard properties are properly handled in json_schema_extra.""" schema = { "type": "array", "items": {"type": "string"}, "minItems": 1, "customField": "customValue", "anotherCustom": 123 } Model = create_model(schema) # Standard array properties (items, minItems) should not be in json_schema_extra # Only custom properties should be there if hasattr(Model, 'model_config') and Model.model_config: config_dict = Model.model_config if 'json_schema_extra' in config_dict: extra = config_dict['json_schema_extra'] # Custom properties should be present assert "customField" in extra assert extra["customField"] == "customValue" assert "anotherCustom" in extra assert extra["anotherCustom"] == 123 # Standard properties should not be in extra assert "items" not in extra assert "minItems" not in extra assert "type" not in extra # Model should still work correctly model = Model.model_validate(["a", "b"]) assert model.root == ["a", "b"] json-schema-to-pydantic-0.4.9/uv.lock000066400000000000000000002156251513773571500175210ustar00rootroot00000000000000version = 1 revision = 3 requires-python = ">=3.9" [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "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, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379, upload-time = "2025-03-30T20:34:53.904Z" }, { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814, upload-time = "2025-03-30T20:34:56.959Z" }, { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937, upload-time = "2025-03-30T20:34:58.751Z" }, { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849, upload-time = "2025-03-30T20:35:00.521Z" }, { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986, upload-time = "2025-03-30T20:35:02.307Z" }, { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896, upload-time = "2025-03-30T20:35:04.141Z" }, { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613, upload-time = "2025-03-30T20:35:05.889Z" }, { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909, upload-time = "2025-03-30T20:35:07.76Z" }, { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948, upload-time = "2025-03-30T20:35:09.144Z" }, { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844, upload-time = "2025-03-30T20:35:10.734Z" }, { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493, upload-time = "2025-03-30T20:35:12.286Z" }, { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921, upload-time = "2025-03-30T20:35:14.18Z" }, { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556, upload-time = "2025-03-30T20:35:15.616Z" }, { url = "https://files.pythonhosted.org/packages/89/97/dcd5c2ce72cee9d7b0ee8c89162c24972fb987a111b92d1a3d1d19100c61/coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040", size = 242245, upload-time = "2025-03-30T20:35:18.648Z" }, { url = "https://files.pythonhosted.org/packages/b2/7b/b63cbb44096141ed435843bbb251558c8e05cc835c8da31ca6ffb26d44c0/coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543", size = 244032, upload-time = "2025-03-30T20:35:20.131Z" }, { url = "https://files.pythonhosted.org/packages/97/e3/7fa8c2c00a1ef530c2a42fa5df25a6971391f92739d83d67a4ee6dcf7a02/coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2", size = 243679, upload-time = "2025-03-30T20:35:21.636Z" }, { url = "https://files.pythonhosted.org/packages/4f/b3/e0a59d8df9150c8a0c0841d55d6568f0a9195692136c44f3d21f1842c8f6/coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318", size = 241852, upload-time = "2025-03-30T20:35:23.525Z" }, { url = "https://files.pythonhosted.org/packages/9b/82/db347ccd57bcef150c173df2ade97976a8367a3be7160e303e43dd0c795f/coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9", size = 242389, upload-time = "2025-03-30T20:35:25.09Z" }, { url = "https://files.pythonhosted.org/packages/21/f6/3f7d7879ceb03923195d9ff294456241ed05815281f5254bc16ef71d6a20/coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c", size = 213997, upload-time = "2025-03-30T20:35:26.914Z" }, { url = "https://files.pythonhosted.org/packages/28/87/021189643e18ecf045dbe1e2071b2747901f229df302de01c998eeadf146/coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78", size = 214911, upload-time = "2025-03-30T20:35:28.498Z" }, { url = "https://files.pythonhosted.org/packages/aa/12/4792669473297f7973518bec373a955e267deb4339286f882439b8535b39/coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc", size = 211684, upload-time = "2025-03-30T20:35:29.959Z" }, { url = "https://files.pythonhosted.org/packages/be/e1/2a4ec273894000ebedd789e8f2fc3813fcaf486074f87fd1c5b2cb1c0a2b/coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6", size = 211935, upload-time = "2025-03-30T20:35:31.912Z" }, { url = "https://files.pythonhosted.org/packages/f8/3a/7b14f6e4372786709a361729164125f6b7caf4024ce02e596c4a69bccb89/coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d", size = 245994, upload-time = "2025-03-30T20:35:33.455Z" }, { url = "https://files.pythonhosted.org/packages/54/80/039cc7f1f81dcbd01ea796d36d3797e60c106077e31fd1f526b85337d6a1/coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05", size = 242885, upload-time = "2025-03-30T20:35:35.354Z" }, { url = "https://files.pythonhosted.org/packages/10/e0/dc8355f992b6cc2f9dcd5ef6242b62a3f73264893bc09fbb08bfcab18eb4/coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a", size = 245142, upload-time = "2025-03-30T20:35:37.121Z" }, { url = "https://files.pythonhosted.org/packages/43/1b/33e313b22cf50f652becb94c6e7dae25d8f02e52e44db37a82de9ac357e8/coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6", size = 244906, upload-time = "2025-03-30T20:35:39.07Z" }, { url = "https://files.pythonhosted.org/packages/05/08/c0a8048e942e7f918764ccc99503e2bccffba1c42568693ce6955860365e/coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47", size = 243124, upload-time = "2025-03-30T20:35:40.598Z" }, { url = "https://files.pythonhosted.org/packages/5b/62/ea625b30623083c2aad645c9a6288ad9fc83d570f9adb913a2abdba562dd/coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe", size = 244317, upload-time = "2025-03-30T20:35:42.204Z" }, { url = "https://files.pythonhosted.org/packages/62/cb/3871f13ee1130a6c8f020e2f71d9ed269e1e2124aa3374d2180ee451cee9/coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545", size = 214170, upload-time = "2025-03-30T20:35:44.216Z" }, { url = "https://files.pythonhosted.org/packages/88/26/69fe1193ab0bfa1eb7a7c0149a066123611baba029ebb448500abd8143f9/coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b", size = 214969, upload-time = "2025-03-30T20:35:45.797Z" }, { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" }, { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" }, { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" }, { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" }, { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" }, { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" }, { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" }, { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" }, { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" }, { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" }, { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" }, { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" }, { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" }, { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" }, { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" }, { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" }, { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" }, { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" }, { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" }, { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" }, { url = "https://files.pythonhosted.org/packages/60/0c/5da94be095239814bf2730a28cffbc48d6df4304e044f80d39e1ae581997/coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f", size = 211377, upload-time = "2025-03-30T20:36:23.298Z" }, { url = "https://files.pythonhosted.org/packages/d5/cb/b9e93ebf193a0bb89dbcd4f73d7b0e6ecb7c1b6c016671950e25f041835e/coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a", size = 211803, upload-time = "2025-03-30T20:36:25.74Z" }, { url = "https://files.pythonhosted.org/packages/78/1a/cdbfe9e1bb14d3afcaf6bb6e1b9ba76c72666e329cd06865bbd241efd652/coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82", size = 240561, upload-time = "2025-03-30T20:36:27.548Z" }, { url = "https://files.pythonhosted.org/packages/59/04/57f1223f26ac018d7ce791bfa65b0c29282de3e041c1cd3ed430cfeac5a5/coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814", size = 238488, upload-time = "2025-03-30T20:36:29.175Z" }, { url = "https://files.pythonhosted.org/packages/b7/b1/0f25516ae2a35e265868670384feebe64e7857d9cffeeb3887b0197e2ba2/coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c", size = 239589, upload-time = "2025-03-30T20:36:30.876Z" }, { url = "https://files.pythonhosted.org/packages/e0/a4/99d88baac0d1d5a46ceef2dd687aac08fffa8795e4c3e71b6f6c78e14482/coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd", size = 239366, upload-time = "2025-03-30T20:36:32.563Z" }, { url = "https://files.pythonhosted.org/packages/ea/9e/1db89e135feb827a868ed15f8fc857160757f9cab140ffee21342c783ceb/coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4", size = 237591, upload-time = "2025-03-30T20:36:34.721Z" }, { url = "https://files.pythonhosted.org/packages/1b/6d/ac4d6fdfd0e201bc82d1b08adfacb1e34b40d21a22cdd62cfaf3c1828566/coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899", size = 238572, upload-time = "2025-03-30T20:36:36.805Z" }, { url = "https://files.pythonhosted.org/packages/25/5e/917cbe617c230f7f1745b6a13e780a3a1cd1cf328dbcd0fd8d7ec52858cd/coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f", size = 213966, upload-time = "2025-03-30T20:36:38.551Z" }, { url = "https://files.pythonhosted.org/packages/bd/93/72b434fe550135869f9ea88dd36068af19afce666db576e059e75177e813/coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3", size = 214852, upload-time = "2025-03-30T20:36:40.209Z" }, { url = "https://files.pythonhosted.org/packages/c4/f1/1da77bb4c920aa30e82fa9b6ea065da3467977c2e5e032e38e66f1c57ffd/coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd", size = 203443, upload-time = "2025-03-30T20:36:41.959Z" }, { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "json-schema-to-pydantic" source = { editable = "." } dependencies = [ { name = "pydantic" }, ] [package.optional-dependencies] dev = [ { name = "pytest" }, { name = "pytest-cov" }, ] [package.metadata] requires-dist = [ { name = "pydantic", specifier = ">=2.10.4" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.4" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0.0" }, ] provides-extras = ["dev"] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pydantic" version = "2.11.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" }, ] [[package]] name = "pydantic-core" version = "2.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, { url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677, upload-time = "2025-04-23T18:32:27.227Z" }, { url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735, upload-time = "2025-04-23T18:32:29.019Z" }, { url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467, upload-time = "2025-04-23T18:32:31.119Z" }, { url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041, upload-time = "2025-04-23T18:32:33.655Z" }, { url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503, upload-time = "2025-04-23T18:32:35.519Z" }, { url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079, upload-time = "2025-04-23T18:32:37.659Z" }, { url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508, upload-time = "2025-04-23T18:32:39.637Z" }, { url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693, upload-time = "2025-04-23T18:32:41.818Z" }, { url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224, upload-time = "2025-04-23T18:32:44.033Z" }, { url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403, upload-time = "2025-04-23T18:32:45.836Z" }, { url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331, upload-time = "2025-04-23T18:32:47.618Z" }, { url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571, upload-time = "2025-04-23T18:32:49.401Z" }, { url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504, upload-time = "2025-04-23T18:32:51.287Z" }, { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, { url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034, upload-time = "2025-04-23T18:33:32.843Z" }, { url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578, upload-time = "2025-04-23T18:33:34.912Z" }, { url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858, upload-time = "2025-04-23T18:33:36.933Z" }, { url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498, upload-time = "2025-04-23T18:33:38.997Z" }, { url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428, upload-time = "2025-04-23T18:33:41.18Z" }, { url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854, upload-time = "2025-04-23T18:33:43.446Z" }, { url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859, upload-time = "2025-04-23T18:33:45.56Z" }, { url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059, upload-time = "2025-04-23T18:33:47.735Z" }, { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" }, ] [[package]] name = "pytest" version = "8.3.5" 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/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] name = "pytest-cov" version = "6.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] name = "typing-extensions" version = "4.13.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, ] [[package]] name = "typing-inspection" version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, ]