diff --git a/README.md b/README.md index cf63364..fc654d3 100644 --- a/README.md +++ b/README.md @@ -212,17 +212,21 @@ Both use suffix matching, so you can omit leading path components. ## Supported JSON Schema types -| JSON Schema type | validataclass | dataclass / pydantic | -|-------------------------|-----------------------------------------------------------------|------------------------------------------------------| -| `boolean` | `BooleanValidator()` | `bool` | -| `integer` | `IntegerValidator(min_value=..., ...)` | `int` / `Annotated[int, Field(ge=..., ...)]` | -| `number` | `FloatValidator(min_value=..., ...)` | `float` / `Annotated[float, Field(ge=..., ...)]` | -| `string` | `StringValidator(min_length=..., ...)` | `str` / `Annotated[str, Field(min_length=..., ...)]` | -| `string` with `pattern` | `RegexValidator(pattern=r'...')` | `str` / `Annotated[str, Field(pattern=r'...')]` | -| `enum` | `EnumValidator(EnumClassName)` | `EnumClassName` | -| `array` | `ListValidator(inner_validator)` | `list[inner_type]` | -| `object` | `DataclassValidator(ClassName)` | `ClassName` | -| `$ref` | Resolved to the referenced type with property overrides applied | +| JSON Schema type | validataclass | dataclass | pydantic | +|-------------------------------|-----------------------------------------------------------------|------------------------------------------------------|------------------------------------------------------| +| `boolean` | `BooleanValidator()` | `bool` | `bool` | +| `integer` | `IntegerValidator(min_value=..., ...)` | `int` | `int` / `Annotated[int, Field(ge=..., ...)]` | +| `number` | `FloatValidator(min_value=..., ...)` | `float` | `float` / `Annotated[float, Field(ge=..., ...)]` | +| `string` | `StringValidator(min_length=..., ...)` | `str` | `str` / `Annotated[str, Field(min_length=..., ...)]` | +| `string` with `pattern` | `RegexValidator(pattern=r'...')` | `str` | `Annotated[str, Field(pattern=r'...')]` | +| `string` format `date-time` | `DateTimeValidator()` | `datetime` | `datetime` | +| `string` format `time` | `TimeValidator()` | `time` | `time` | +| `string` format `email` | `EmailValidator()` | `str` | `EmailStr` | +| `string` format `uri` | `UrlValidator()` | `str` | `AnyUrl` | +| `enum` | `EnumValidator(EnumClassName)` | `EnumClassName` | `EnumClassName` | +| `array` | `ListValidator(inner_validator)` | `list[inner_type]` | `list[inner_type]` | +| `object` | `DataclassValidator(ClassName)` | `ClassName` | `ClassName` | +| `$ref` | Resolved to the referenced type with property overrides applied | ## Development diff --git a/dev/build-shared.py b/dev/build-shared.py new file mode 100644 index 0000000..1675edc --- /dev/null +++ b/dev/build-shared.py @@ -0,0 +1,14 @@ +""" +Copyright 2025 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +import sys +from pathlib import Path + +sys.path.append(str(Path(Path(__file__).parent.parent, 'src'))) # noqa: E402 + +from schema2classes.scripts.build_shared import main + +if __name__ == '__main__': + main() diff --git a/pyproject.toml b/pyproject.toml index 837599f..857aa53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ Issues = "https://github.com/datex2-tools/schema2classes/issues" [dependency-groups] dev = [ "validataclass~=0.11.0", + "pydantic[email]>=2.0", "pytest~=9.0.2", "pytest-cov~=7.1.0", ] @@ -134,6 +135,8 @@ line-length = 120 "src/schema2classes/scripts/*" = [ # Don't require __init__.py files "INP", + # Allow print in CLI scripts + "T201", ] "dev/*" = [ # Don't require __init__.py files diff --git a/src/schema2classes/common/uri.py b/src/schema2classes/common/uri.py index 486dc82..ceb419b 100644 --- a/src/schema2classes/common/uri.py +++ b/src/schema2classes/common/uri.py @@ -59,6 +59,10 @@ def from_reference(cls, uri: 'URI', reference: str) -> 'URI': if reference.startswith('http'): return cls(url=location, json_path=json_path) + # Internal reference (e.g. "#/$defs/Foo"): same file as the parent URI + if location == '': + return cls(file_path=uri.file_path, url=uri.url, json_path=json_path) + # TODO: local file path in a schema via URL if location.startswith('/'): return cls(file_path=Path(location), json_path=json_path) diff --git a/src/schema2classes/output/base_outputs.py b/src/schema2classes/output/base_outputs.py index f25c442..c048d25 100644 --- a/src/schema2classes/output/base_outputs.py +++ b/src/schema2classes/output/base_outputs.py @@ -16,12 +16,16 @@ Array, BaseField, Boolean, + DateTime, + Email, Enum, Integer, Number, Object, Reference, String, + Time, + Uri, ) logger = logging.getLogger(__name__) @@ -203,6 +207,34 @@ def get_type() -> str: return 'str' +@dataclass(kw_only=True, init=False) +class DateTimeBaseOutput(BaseOutput, ABC): + @staticmethod + def get_type() -> str: + return 'datetime' + + +@dataclass(kw_only=True, init=False) +class TimeBaseOutput(BaseOutput, ABC): + @staticmethod + def get_type() -> str: + return 'time' + + +@dataclass(kw_only=True, init=False) +class EmailBaseOutput(BaseOutput, ABC): + @staticmethod + def get_type() -> str: + return 'str' + + +@dataclass(kw_only=True, init=False) +class UriBaseOutput(BaseOutput, ABC): + @staticmethod + def get_type() -> str: + return 'str' + + @dataclass(kw_only=True, init=False) class ListBaseOutput(BaseOutput, ABC): output: BaseOutput @@ -367,6 +399,14 @@ def determine_output(field: BaseField, output_classes: dict) -> type[BaseOutput] return output_classes['integer'] if isinstance(field, Number): return output_classes['float'] + if isinstance(field, DateTime): + return output_classes['datetime'] + if isinstance(field, Time): + return output_classes['time'] + if isinstance(field, Email): + return output_classes['email'] + if isinstance(field, Uri): + return output_classes['uri'] if isinstance(field, String) and field.pattern is not None: return output_classes['regex'] if isinstance(field, String) and field.pattern is None: diff --git a/src/schema2classes/output/dataclass_outputs.py b/src/schema2classes/output/dataclass_outputs.py index 007c09f..51695a2 100644 --- a/src/schema2classes/output/dataclass_outputs.py +++ b/src/schema2classes/output/dataclass_outputs.py @@ -12,6 +12,8 @@ from .base_outputs import ( BooleanBaseOutput, + DateTimeBaseOutput, + EmailBaseOutput, EnumBaseOutput, FloatBaseOutput, IntegerBaseOutput, @@ -20,6 +22,8 @@ ObjectBaseOutput, RegexBaseOutput, StringBaseOutput, + TimeBaseOutput, + UriBaseOutput, ) @@ -79,6 +83,28 @@ class DataclassRegexOutput(DataclassRenderMixin, RegexBaseOutput): pass +@dataclass(kw_only=True, init=False) +class DataclassDateTimeOutput(DataclassRenderMixin, DateTimeBaseOutput): + def get_imports(self) -> list[str]: + return ['datetime.datetime'] + + +@dataclass(kw_only=True, init=False) +class DataclassTimeOutput(DataclassRenderMixin, TimeBaseOutput): + def get_imports(self) -> list[str]: + return ['datetime.time'] + + +@dataclass(kw_only=True, init=False) +class DataclassEmailOutput(DataclassRenderMixin, EmailBaseOutput): + pass + + +@dataclass(kw_only=True, init=False) +class DataclassUriOutput(DataclassRenderMixin, UriBaseOutput): + pass + + @dataclass(kw_only=True, init=False) class DataclassListOutput(DataclassRenderMixin, ListBaseOutput): def get_imports(self) -> list[str]: @@ -105,6 +131,10 @@ def get_imports(self) -> list[str]: 'integer': DataclassIntegerOutput, 'float': DataclassFloatOutput, 'string': DataclassStringOutput, + 'datetime': DataclassDateTimeOutput, + 'time': DataclassTimeOutput, + 'email': DataclassEmailOutput, + 'uri': DataclassUriOutput, 'enum': DataclassEnumOutput, 'regex': DataclassRegexOutput, 'list': DataclassListOutput, diff --git a/src/schema2classes/output/outputs.py b/src/schema2classes/output/outputs.py index 958d98c..b60342f 100644 --- a/src/schema2classes/output/outputs.py +++ b/src/schema2classes/output/outputs.py @@ -6,6 +6,8 @@ from .base_outputs import ( # noqa: F401 BaseOutput, BooleanBaseOutput, + DateTimeBaseOutput, + EmailBaseOutput, EnumBaseOutput, FloatBaseOutput, IntegerBaseOutput, @@ -14,6 +16,8 @@ ObjectBaseOutput, RegexBaseOutput, StringBaseOutput, + TimeBaseOutput, + UriBaseOutput, determine_output, follow_reference, ) diff --git a/src/schema2classes/output/pydantic_outputs.py b/src/schema2classes/output/pydantic_outputs.py index 359b6c7..2b3a384 100644 --- a/src/schema2classes/output/pydantic_outputs.py +++ b/src/schema2classes/output/pydantic_outputs.py @@ -12,6 +12,8 @@ from .base_outputs import ( BooleanBaseOutput, + DateTimeBaseOutput, + EmailBaseOutput, EnumBaseOutput, FloatBaseOutput, IntegerBaseOutput, @@ -20,6 +22,8 @@ ObjectBaseOutput, RegexBaseOutput, StringBaseOutput, + TimeBaseOutput, + UriBaseOutput, ) @@ -162,6 +166,38 @@ def get_imports(self) -> list[str]: return ['typing.Annotated', 'pydantic.Field'] +@dataclass(kw_only=True, init=False) +class PydanticDateTimeOutput(PydanticRenderMixin, DateTimeBaseOutput): + def get_imports(self) -> list[str]: + return ['datetime.datetime'] + + +@dataclass(kw_only=True, init=False) +class PydanticTimeOutput(PydanticRenderMixin, TimeBaseOutput): + def get_imports(self) -> list[str]: + return ['datetime.time'] + + +@dataclass(kw_only=True, init=False) +class PydanticEmailOutput(PydanticRenderMixin, EmailBaseOutput): + @staticmethod + def get_type() -> str: + return 'EmailStr' + + def get_imports(self) -> list[str]: + return ['pydantic.EmailStr'] + + +@dataclass(kw_only=True, init=False) +class PydanticUriOutput(PydanticRenderMixin, UriBaseOutput): + @staticmethod + def get_type() -> str: + return 'AnyUrl' + + def get_imports(self) -> list[str]: + return ['pydantic.AnyUrl'] + + @dataclass(kw_only=True, init=False) class PydanticListOutput(PydanticRenderMixin, ListBaseOutput): def get_imports(self) -> list[str]: @@ -190,6 +226,10 @@ def get_imports(self) -> list[str]: 'integer': PydanticIntegerOutput, 'float': PydanticFloatOutput, 'string': PydanticStringOutput, + 'datetime': PydanticDateTimeOutput, + 'time': PydanticTimeOutput, + 'email': PydanticEmailOutput, + 'uri': PydanticUriOutput, 'enum': PydanticEnumOutput, 'regex': PydanticRegexOutput, 'list': PydanticListOutput, diff --git a/src/schema2classes/output/validataclass_outputs.py b/src/schema2classes/output/validataclass_outputs.py index 079cbb1..25c6fa2 100644 --- a/src/schema2classes/output/validataclass_outputs.py +++ b/src/schema2classes/output/validataclass_outputs.py @@ -13,6 +13,8 @@ from .base_outputs import ( BooleanBaseOutput, + DateTimeBaseOutput, + EmailBaseOutput, EnumBaseOutput, FloatBaseOutput, IntegerBaseOutput, @@ -21,6 +23,8 @@ ObjectBaseOutput, RegexBaseOutput, StringBaseOutput, + TimeBaseOutput, + UriBaseOutput, ) @@ -131,6 +135,42 @@ def get_imports(self) -> list[str]: return self._get_base_imports() + ['validataclass.validators.RegexValidator'] +@dataclass(kw_only=True, init=False) +class ValidataclassDateTimeOutput(ValidataclassRenderMixin, DateTimeBaseOutput): + def render_validator(self) -> str: + return 'DateTimeValidator()' + + def get_imports(self) -> list[str]: + return self._get_base_imports() + ['datetime.datetime', 'validataclass.validators.DateTimeValidator'] + + +@dataclass(kw_only=True, init=False) +class ValidataclassTimeOutput(ValidataclassRenderMixin, TimeBaseOutput): + def render_validator(self) -> str: + return 'TimeValidator()' + + def get_imports(self) -> list[str]: + return self._get_base_imports() + ['datetime.time', 'validataclass.validators.TimeValidator'] + + +@dataclass(kw_only=True, init=False) +class ValidataclassEmailOutput(ValidataclassRenderMixin, EmailBaseOutput): + def render_validator(self) -> str: + return 'EmailValidator()' + + def get_imports(self) -> list[str]: + return self._get_base_imports() + ['validataclass.validators.EmailValidator'] + + +@dataclass(kw_only=True, init=False) +class ValidataclassUriOutput(ValidataclassRenderMixin, UriBaseOutput): + def render_validator(self) -> str: + return 'UrlValidator()' + + def get_imports(self) -> list[str]: + return self._get_base_imports() + ['validataclass.validators.UrlValidator'] + + @dataclass(kw_only=True, init=False) class ValidataclassListOutput(ValidataclassRenderMixin, ListBaseOutput): def render_validator(self) -> str: @@ -166,6 +206,10 @@ def get_imports(self) -> list[str]: 'integer': ValidataclassIntegerOutput, 'float': ValidataclassFloatOutput, 'string': ValidataclassStringOutput, + 'datetime': ValidataclassDateTimeOutput, + 'time': ValidataclassTimeOutput, + 'email': ValidataclassEmailOutput, + 'uri': ValidataclassUriOutput, 'enum': ValidataclassEnumOutput, 'regex': ValidataclassRegexOutput, 'list': ValidataclassListOutput, diff --git a/src/schema2classes/schema/models.py b/src/schema2classes/schema/models.py index 02e76b4..45d8825 100644 --- a/src/schema2classes/schema/models.py +++ b/src/schema2classes/schema/models.py @@ -89,6 +89,26 @@ def __init__(self, schema: dict, **kwargs): self.exclusiveMaximum = schema.get('exclusiveMaximum') +@dataclass(kw_only=True, init=False) +class DateTime(BaseField): + default: str | None = None + + +@dataclass(kw_only=True, init=False) +class Time(BaseField): + default: str | None = None + + +@dataclass(kw_only=True, init=False) +class Email(BaseField): + default: str | None = None + + +@dataclass(kw_only=True, init=False) +class Uri(BaseField): + default: str | None = None + + @dataclass(kw_only=True, init=False) class Enum(BaseField): default: str | None = None @@ -230,6 +250,19 @@ def get_field_by_uri(self, uri: URI) -> BaseField | None: def parse_schema(schema: dict, **kwargs) -> BaseField: + # oneOf/anyOf: pick the first alternative, preferring one without an enum constraint. + # Parent-level title/description/default propagate to the chosen alternative. + for combiner in ('oneOf', 'anyOf'): + alternatives = schema.get(combiner) + if not isinstance(alternatives, list) or not alternatives: + continue + chosen = next((alt for alt in alternatives if isinstance(alt, dict) and 'enum' not in alt), alternatives[0]) + merged = dict(chosen) + for key in ('title', 'description', 'default'): + if key not in merged and schema.get(key) is not None: + merged[key] = schema[key] + return parse_schema(merged, **kwargs) + # Special cases without type if schema.get('enum') is not None: return Enum(schema, **kwargs) @@ -244,6 +277,15 @@ def parse_schema(schema: dict, **kwargs) -> BaseField: if schema.get('type') == 'number': return Number(schema, **kwargs) if schema.get('type') == 'string': + match schema.get('format'): + case 'date-time': + return DateTime(schema, **kwargs) + case 'time': + return Time(schema, **kwargs) + case 'email': + return Email(schema, **kwargs) + case 'uri': + return Uri(schema, **kwargs) return String(schema, **kwargs) if schema.get('type') == 'array': return Array(schema, **kwargs) diff --git a/src/schema2classes/scripts/build_shared.py b/src/schema2classes/scripts/build_shared.py new file mode 100644 index 0000000..f4c18b7 --- /dev/null +++ b/src/schema2classes/scripts/build_shared.py @@ -0,0 +1,146 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +import argparse +import filecmp +import re +import shutil +from pathlib import Path + + +def find_identical_files(dir1: Path, dir2: Path) -> set[str]: + """Find Python files that exist in both directories with identical content.""" + files1 = {f.name for f in dir1.glob('*.py') if f.name != '__init__.py'} + files2 = {f.name for f in dir2.glob('*.py') if f.name != '__init__.py'} + common = files1 & files2 + + return {name for name in common if filecmp.cmp(dir1 / name, dir2 / name, shallow=False)} + + +def get_relative_imports(file_path: Path) -> set[str]: + """Extract module names from relative imports (e.g. 'from .module import Class').""" + content = file_path.read_text() + return {match.group(1) for match in re.finditer(r'^from \.([\w]+) import', content, re.MULTILINE)} + + +def compute_movable_files(identical_files: set[str], dir1: Path) -> set[str]: + """ + Iteratively compute which identical files can be moved to shared/. + + A file is movable only if all its relative imports also point to movable files, + ensuring the shared directory is self-contained. + """ + movable = set(identical_files) + changed = True + while changed: + changed = False + for name in list(movable): + imports = get_relative_imports(dir1 / name) + required_files = {module + '.py' for module in imports} + if not required_files.issubset(movable): + movable.discard(name) + changed = True + return movable + + +def fix_imports_in_file(file_path: Path, shared_modules: set[str]): + """Rewrite relative imports to point to ..shared where the module was moved.""" + content = file_path.read_text() + + def replace_import(match: re.Match) -> str: + module = match.group(2) + if module in shared_modules: + return f'{match.group(1)}..shared.{module}{match.group(3)}' + return match.group(0) + + new_content = re.sub( + r'^(from )\.(\w+)( import)', + replace_import, + content, + flags=re.MULTILINE, + ) + + if new_content != content: + file_path.write_text(new_content) + + +def main(): + parser = argparse.ArgumentParser( + description='Move identical files from two source directories into a shared directory and fix imports.', + ) + parser.add_argument('dir1', type=Path, help='First source directory') + parser.add_argument('dir2', type=Path, help='Second source directory') + parser.add_argument('shared', type=Path, help='Shared output directory') + args = parser.parse_args() + + dir1 = args.dir1.resolve() + dir2 = args.dir2.resolve() + shared_dir = args.shared.resolve() + + for directory in [dir1, dir2]: + if not directory.is_dir(): + parser.error(f'Directory does not exist: {directory}') + + # Step 1: find identical files + identical = find_identical_files(dir1, dir2) + print(f'Found {len(identical)} identical files between the two directories') + + if not identical: + print('Nothing to do.') + return + + # Step 2: compute which files can safely be moved (self-contained imports) + movable = compute_movable_files(identical, dir1) + print(f'{len(movable)} files can be moved to shared/') + + skipped = identical - movable + if skipped: + print(f'Skipping {len(skipped)} identical files (they import non-shared modules):') + for name in sorted(skipped): + print(f' {name}') + + if not movable: + print('No files to move.') + return + + # Step 3: move files to shared directory + shared_dir.mkdir(parents=True, exist_ok=True) + shared_modules = {name.removesuffix('.py') for name in movable} + + for name in sorted(movable): + shutil.move(dir1 / name, shared_dir / name) + (dir2 / name).unlink() + + print(f'Moved {len(movable)} files to {shared_dir}') + + # Step 4: create __init__.py in shared directory + init_path = shared_dir / '__init__.py' + if not init_path.exists(): + init_content = '' + # copy style from source directory if available + for source_init in [dir1 / '__init__.py', dir2 / '__init__.py']: + if source_init.exists(): + init_content = source_init.read_text() + break + init_path.write_text(init_content) + print(f'Created {init_path}') + + # Step 5: fix imports in remaining source directory files + fixed_count = 0 + for source_dir in [dir1, dir2]: + for py_file in source_dir.glob('*.py'): + if py_file.name == '__init__.py': + continue + content_before = py_file.read_text() + fix_imports_in_file(py_file, shared_modules) + if py_file.read_text() != content_before: + fixed_count += 1 + + print(f'Fixed imports in {fixed_count} files') + print('Done.') + + +if __name__ == '__main__': + main() diff --git a/tests/integration/dataclass/helpers.py b/tests/integration/dataclass/helpers.py index 93ed6a9..61c7859 100644 --- a/tests/integration/dataclass/helpers.py +++ b/tests/integration/dataclass/helpers.py @@ -3,7 +3,10 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. """ +import importlib +import sys from pathlib import Path +from types import ModuleType from schema2classes import App from schema2classes.common.uri import URI @@ -20,3 +23,14 @@ def run_generate(schema_path: Path, output_path: Path): config = Config(output_format=OutputFormat.DATACLASS, post_processing=[]) app = App(config=config) app.generate(URI(file_path=schema_path), output_path) + + +def import_module(output_path: Path, module_name: str) -> ModuleType: + pkg_name = output_path.name + parent = str(output_path.parent) + if parent not in sys.path: + sys.path.insert(0, parent) + for key in [k for k in sys.modules if k == pkg_name or k.startswith(f'{pkg_name}.')]: + del sys.modules[key] + importlib.import_module(pkg_name) + return importlib.import_module(f'{pkg_name}.{module_name}') diff --git a/tests/integration/dataclass/simple_formats_test.py b/tests/integration/dataclass/simple_formats_test.py new file mode 100644 index 0000000..df8c293 --- /dev/null +++ b/tests/integration/dataclass/simple_formats_test.py @@ -0,0 +1,91 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from datetime import datetime, time +from pathlib import Path + +from tests.integration.dataclass.helpers import INPUT_DIR, generated_files, import_module, run_generate + +SCHEMA_PATH = INPUT_DIR / 'simple_formats.json' + + +def test_generates_expected_files(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + assert generated_files(tmp_path) == {'__init__.py', 'simple_formats_input.py'} + + +def test_required_datetime_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_required_datetime: datetime\n' in content or 'test_required_datetime: datetime' in content + assert 'test_required_datetime: datetime |' not in content + + +def test_optional_datetime_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_datetime: datetime | None = None' in content + + +def test_optional_time_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_time: time | None = None' in content + + +def test_optional_email_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_email: str | None = None' in content + + +def test_optional_uri_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_uri: str | None = None' in content + + +def test_datetime_imports(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'from datetime import' in content + + +def test_import_and_instantiate_all_fields(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + mod = import_module(tmp_path, 'simple_formats_input') + + obj = mod.SimpleFormatsInput( + test_required_datetime=datetime(2025, 1, 1), + test_datetime=datetime(2025, 6, 15, 12, 30), + test_time=time(14, 30), + test_email='user@example.com', + test_uri='https://example.com', + ) + + assert obj.test_required_datetime == datetime(2025, 1, 1) + assert obj.test_datetime == datetime(2025, 6, 15, 12, 30) + assert obj.test_time == time(14, 30) + assert obj.test_email == 'user@example.com' + assert obj.test_uri == 'https://example.com' + + +def test_import_and_instantiate_required_only(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + mod = import_module(tmp_path, 'simple_formats_input') + + obj = mod.SimpleFormatsInput(test_required_datetime=datetime(2025, 1, 1)) + + assert obj.test_required_datetime == datetime(2025, 1, 1) + assert obj.test_datetime is None + assert obj.test_time is None + assert obj.test_email is None + assert obj.test_uri is None diff --git a/tests/integration/pydantic/helpers.py b/tests/integration/pydantic/helpers.py index 3068479..92d6c86 100644 --- a/tests/integration/pydantic/helpers.py +++ b/tests/integration/pydantic/helpers.py @@ -3,7 +3,10 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. """ +import importlib +import sys from pathlib import Path +from types import ModuleType from schema2classes import App from schema2classes.common.uri import URI @@ -20,3 +23,14 @@ def run_generate(schema_path: Path, output_path: Path): config = Config(output_format=OutputFormat.PYDANTIC, post_processing=[]) app = App(config=config) app.generate(URI(file_path=schema_path), output_path) + + +def import_module(output_path: Path, module_name: str) -> ModuleType: + pkg_name = output_path.name + parent = str(output_path.parent) + if parent not in sys.path: + sys.path.insert(0, parent) + for key in [k for k in sys.modules if k == pkg_name or k.startswith(f'{pkg_name}.')]: + del sys.modules[key] + importlib.import_module(pkg_name) + return importlib.import_module(f'{pkg_name}.{module_name}') diff --git a/tests/integration/pydantic/simple_formats_test.py b/tests/integration/pydantic/simple_formats_test.py new file mode 100644 index 0000000..25bb0a6 --- /dev/null +++ b/tests/integration/pydantic/simple_formats_test.py @@ -0,0 +1,97 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from datetime import datetime, time +from pathlib import Path + +from pydantic import AnyUrl + +from tests.integration.pydantic.helpers import INPUT_DIR, generated_files, import_module, run_generate + +SCHEMA_PATH = INPUT_DIR / 'simple_formats.json' + + +def test_generates_expected_files(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + assert generated_files(tmp_path) == {'__init__.py', 'simple_formats_input.py'} + + +def test_required_datetime_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_required_datetime: datetime' in content + assert 'test_required_datetime: datetime |' not in content + + +def test_optional_datetime_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_datetime: datetime | None = None' in content + + +def test_optional_time_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_time: time | None = None' in content + + +def test_optional_email_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_email: EmailStr | None = None' in content + + +def test_optional_uri_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_uri: AnyUrl | None = None' in content + + +def test_imports(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'from datetime import' in content + assert 'EmailStr' in content + assert 'AnyUrl' in content + + +def test_validate_all_fields(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + mod = import_module(tmp_path, 'simple_formats_input') + + result = mod.SimpleFormatsInput.model_validate({ + 'test_required_datetime': '2025-01-01T00:00:00Z', + 'test_datetime': '2025-06-15T12:30:00Z', + 'test_time': '14:30:00', + 'test_email': 'user@example.com', + 'test_uri': 'https://example.com', + }) + + assert result.test_required_datetime == datetime(2025, 1, 1, tzinfo=result.test_required_datetime.tzinfo) + assert result.test_datetime == datetime(2025, 6, 15, 12, 30, tzinfo=result.test_datetime.tzinfo) + assert result.test_time == time(14, 30) + assert result.test_email == 'user@example.com' + assert isinstance(result.test_uri, AnyUrl) + + +def test_validate_required_only(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + mod = import_module(tmp_path, 'simple_formats_input') + + result = mod.SimpleFormatsInput.model_validate({ + 'test_required_datetime': '2025-01-01T00:00:00Z', + }) + + assert isinstance(result.test_required_datetime, datetime) + assert result.test_datetime is None + assert result.test_time is None + assert result.test_email is None + assert result.test_uri is None diff --git a/tests/integration/validataclass/helpers.py b/tests/integration/validataclass/helpers.py index d155ef8..f0970e1 100644 --- a/tests/integration/validataclass/helpers.py +++ b/tests/integration/validataclass/helpers.py @@ -3,7 +3,10 @@ Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. """ +import importlib +import sys from pathlib import Path +from types import ModuleType from schema2classes import App from schema2classes.common.uri import URI @@ -20,3 +23,14 @@ def run_generate(schema_path: Path, output_path: Path): config = Config(post_processing=[]) app = App(config=config) app.generate(URI(file_path=schema_path), output_path) + + +def import_module(output_path: Path, module_name: str) -> ModuleType: + pkg_name = output_path.name + parent = str(output_path.parent) + if parent not in sys.path: + sys.path.insert(0, parent) + for key in [k for k in sys.modules if k == pkg_name or k.startswith(f'{pkg_name}.')]: + del sys.modules[key] + importlib.import_module(pkg_name) + return importlib.import_module(f'{pkg_name}.{module_name}') diff --git a/tests/integration/validataclass/simple_formats_test.py b/tests/integration/validataclass/simple_formats_test.py new file mode 100644 index 0000000..7f5ca07 --- /dev/null +++ b/tests/integration/validataclass/simple_formats_test.py @@ -0,0 +1,101 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from datetime import datetime, time +from pathlib import Path + +from validataclass.helpers import UnsetValue +from validataclass.validators import DataclassValidator + +from tests.integration.validataclass.helpers import INPUT_DIR, generated_files, import_module, run_generate + +SCHEMA_PATH = INPUT_DIR / 'simple_formats.json' + + +def test_generates_expected_files(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + assert generated_files(tmp_path) == {'__init__.py', 'simple_formats_input.py'} + + +def test_required_datetime_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_required_datetime: datetime = DateTimeValidator()' in content + + +def test_optional_datetime_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_datetime: datetime | UnsetValueType = DateTimeValidator(), Default(UnsetValue)' in content + + +def test_optional_time_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_time: time | UnsetValueType = TimeValidator(), Default(UnsetValue)' in content + + +def test_optional_email_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_email: str | UnsetValueType = EmailValidator(), Default(UnsetValue)' in content + + +def test_optional_uri_field(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'test_uri: str | UnsetValueType = UrlValidator(), Default(UnsetValue)' in content + + +def test_imports(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + content = (tmp_path / 'simple_formats_input.py').read_text() + + assert 'DateTimeValidator' in content + assert 'TimeValidator' in content + assert 'EmailValidator' in content + assert 'UrlValidator' in content + assert 'from datetime import' in content + + +def test_validate_all_fields(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + mod = import_module(tmp_path, 'simple_formats_input') + + validator = DataclassValidator(mod.SimpleFormatsInput) + result = validator.validate({ + 'test_required_datetime': '2025-01-01T00:00:00', + 'test_datetime': '2025-06-15T12:30:00', + 'test_time': '14:30:00', + 'test_email': 'user@example.com', + 'test_uri': 'https://example.com', + }) + + assert result.test_required_datetime == datetime(2025, 1, 1) + assert result.test_datetime == datetime(2025, 6, 15, 12, 30) + assert result.test_time == time(14, 30) + assert result.test_email == 'user@example.com' + assert result.test_uri == 'https://example.com' + + +def test_validate_required_only(tmp_path: Path): + run_generate(SCHEMA_PATH, tmp_path) + mod = import_module(tmp_path, 'simple_formats_input') + + validator = DataclassValidator(mod.SimpleFormatsInput) + result = validator.validate({ + 'test_required_datetime': '2025-01-01T00:00:00', + }) + + assert result.test_required_datetime == datetime(2025, 1, 1) + assert result.test_datetime is UnsetValue + assert result.test_time is UnsetValue + assert result.test_email is UnsetValue + assert result.test_uri is UnsetValue diff --git a/tests/test_schema/input/simple_formats.json b/tests/test_schema/input/simple_formats.json new file mode 100644 index 0000000..0186e9b --- /dev/null +++ b/tests/test_schema/input/simple_formats.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "Simple Formats", + "required": [ + "test_required_datetime" + ], + "properties": { + "test_required_datetime": { + "type": "string", + "format": "date-time" + }, + "test_datetime": { + "type": "string", + "format": "date-time" + }, + "test_time": { + "type": "string", + "format": "time" + }, + "test_email": { + "type": "string", + "format": "email" + }, + "test_uri": { + "type": "string", + "format": "uri" + } + } +} diff --git a/tests/unit/datetime_test.py b/tests/unit/datetime_test.py new file mode 100644 index 0000000..ca8139c --- /dev/null +++ b/tests/unit/datetime_test.py @@ -0,0 +1,26 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from schema2classes.schema.models import DateTime +from tests.unit.helpers import make_uri + + +def test_basic(): + result = DateTime({'type': 'string', 'format': 'date-time'}, uri=make_uri()) + assert result.default is None + + +def test_with_default(): + result = DateTime({'type': 'string', 'format': 'date-time', 'default': '2025-01-01T00:00:00Z'}, uri=make_uri()) + assert result.default == '2025-01-01T00:00:00Z' + + +def test_with_title_and_description(): + result = DateTime( + {'type': 'string', 'format': 'date-time', 'title': 'Created At', 'description': 'Creation timestamp'}, + uri=make_uri(), + ) + assert result.title == 'Created At' + assert result.description == 'Creation timestamp' diff --git a/tests/unit/email_test.py b/tests/unit/email_test.py new file mode 100644 index 0000000..90e8ad3 --- /dev/null +++ b/tests/unit/email_test.py @@ -0,0 +1,26 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from schema2classes.schema.models import Email +from tests.unit.helpers import make_uri + + +def test_basic(): + result = Email({'type': 'string', 'format': 'email'}, uri=make_uri()) + assert result.default is None + + +def test_with_default(): + result = Email({'type': 'string', 'format': 'email', 'default': 'test@example.com'}, uri=make_uri()) + assert result.default == 'test@example.com' + + +def test_with_title_and_description(): + result = Email( + {'type': 'string', 'format': 'email', 'title': 'Contact', 'description': 'Email address'}, + uri=make_uri(), + ) + assert result.title == 'Contact' + assert result.description == 'Email address' diff --git a/tests/unit/parse_schema_datetime_test.py b/tests/unit/parse_schema_datetime_test.py new file mode 100644 index 0000000..399b20c --- /dev/null +++ b/tests/unit/parse_schema_datetime_test.py @@ -0,0 +1,24 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from schema2classes.schema.models import DateTime, parse_schema +from tests.unit.helpers import make_uri + + +def test_returns_datetime(): + result = parse_schema({'type': 'string', 'format': 'date-time'}, uri=make_uri()) + assert isinstance(result, DateTime) + + +def test_datetime_with_title(): + result = parse_schema({'type': 'string', 'format': 'date-time', 'title': 'Created At'}, uri=make_uri()) + assert isinstance(result, DateTime) + assert result.title == 'Created At' + + +def test_datetime_with_default(): + result = parse_schema({'type': 'string', 'format': 'date-time', 'default': '2025-01-01T00:00:00Z'}, uri=make_uri()) + assert isinstance(result, DateTime) + assert result.default == '2025-01-01T00:00:00Z' diff --git a/tests/unit/parse_schema_email_test.py b/tests/unit/parse_schema_email_test.py new file mode 100644 index 0000000..7b20c27 --- /dev/null +++ b/tests/unit/parse_schema_email_test.py @@ -0,0 +1,24 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from schema2classes.schema.models import Email, parse_schema +from tests.unit.helpers import make_uri + + +def test_returns_email(): + result = parse_schema({'type': 'string', 'format': 'email'}, uri=make_uri()) + assert isinstance(result, Email) + + +def test_email_with_title(): + result = parse_schema({'type': 'string', 'format': 'email', 'title': 'Contact'}, uri=make_uri()) + assert isinstance(result, Email) + assert result.title == 'Contact' + + +def test_email_with_default(): + result = parse_schema({'type': 'string', 'format': 'email', 'default': 'test@example.com'}, uri=make_uri()) + assert isinstance(result, Email) + assert result.default == 'test@example.com' diff --git a/tests/unit/parse_schema_time_test.py b/tests/unit/parse_schema_time_test.py new file mode 100644 index 0000000..82cbb9b --- /dev/null +++ b/tests/unit/parse_schema_time_test.py @@ -0,0 +1,24 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from schema2classes.schema.models import Time, parse_schema +from tests.unit.helpers import make_uri + + +def test_returns_time(): + result = parse_schema({'type': 'string', 'format': 'time'}, uri=make_uri()) + assert isinstance(result, Time) + + +def test_time_with_title(): + result = parse_schema({'type': 'string', 'format': 'time', 'title': 'Start Time'}, uri=make_uri()) + assert isinstance(result, Time) + assert result.title == 'Start Time' + + +def test_time_with_default(): + result = parse_schema({'type': 'string', 'format': 'time', 'default': '12:00:00'}, uri=make_uri()) + assert isinstance(result, Time) + assert result.default == '12:00:00' diff --git a/tests/unit/parse_schema_uri_test.py b/tests/unit/parse_schema_uri_test.py new file mode 100644 index 0000000..8a0ae65 --- /dev/null +++ b/tests/unit/parse_schema_uri_test.py @@ -0,0 +1,24 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from schema2classes.schema.models import Uri, parse_schema +from tests.unit.helpers import make_uri + + +def test_returns_uri(): + result = parse_schema({'type': 'string', 'format': 'uri'}, uri=make_uri()) + assert isinstance(result, Uri) + + +def test_uri_with_title(): + result = parse_schema({'type': 'string', 'format': 'uri', 'title': 'Homepage'}, uri=make_uri()) + assert isinstance(result, Uri) + assert result.title == 'Homepage' + + +def test_uri_with_default(): + result = parse_schema({'type': 'string', 'format': 'uri', 'default': 'https://example.com'}, uri=make_uri()) + assert isinstance(result, Uri) + assert result.default == 'https://example.com' diff --git a/tests/unit/time_test.py b/tests/unit/time_test.py new file mode 100644 index 0000000..57608ef --- /dev/null +++ b/tests/unit/time_test.py @@ -0,0 +1,26 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from schema2classes.schema.models import Time +from tests.unit.helpers import make_uri + + +def test_basic(): + result = Time({'type': 'string', 'format': 'time'}, uri=make_uri()) + assert result.default is None + + +def test_with_default(): + result = Time({'type': 'string', 'format': 'time', 'default': '12:00:00'}, uri=make_uri()) + assert result.default == '12:00:00' + + +def test_with_title_and_description(): + result = Time( + {'type': 'string', 'format': 'time', 'title': 'Start Time', 'description': 'The start time'}, + uri=make_uri(), + ) + assert result.title == 'Start Time' + assert result.description == 'The start time' diff --git a/tests/unit/uri_test.py b/tests/unit/uri_test.py new file mode 100644 index 0000000..5c3a85f --- /dev/null +++ b/tests/unit/uri_test.py @@ -0,0 +1,26 @@ +""" +Copyright 2026 binary butterfly GmbH +Use of this source code is governed by an MIT-style license that can be found in the LICENSE.txt. +""" + +from schema2classes.schema.models import Uri +from tests.unit.helpers import make_uri + + +def test_basic(): + result = Uri({'type': 'string', 'format': 'uri'}, uri=make_uri()) + assert result.default is None + + +def test_with_default(): + result = Uri({'type': 'string', 'format': 'uri', 'default': 'https://example.com'}, uri=make_uri()) + assert result.default == 'https://example.com' + + +def test_with_title_and_description(): + result = Uri( + {'type': 'string', 'format': 'uri', 'title': 'Homepage', 'description': 'Website URL'}, + uri=make_uri(), + ) + assert result.title == 'Homepage' + assert result.description == 'Website URL' diff --git a/uv.lock b/uv.lock index 191e53a..b6091ae 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.12" +[[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" @@ -95,6 +104,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -197,6 +237,97 @@ 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.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -318,6 +449,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "pydantic", extra = ["email"] }, { name = "pytest" }, { name = "pytest-cov" }, { name = "validataclass" }, @@ -332,6 +464,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "pydantic", extras = ["email"], specifier = ">=2.0" }, { name = "pytest", specifier = "~=9.0.2" }, { name = "pytest-cov", specifier = "~=7.1.0" }, { name = "validataclass", specifier = "~=0.11.0" }, @@ -346,6 +479,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "validataclass" version = "0.11.0"