25 Commits

Author SHA1 Message Date
81c149120e Merge pull request #50 from fredsonnenwald/string_format
Fix Field deprecation warning resulting from building models with formatted strings
2025-09-15 19:18:20 -03:00
171dddabab Merge pull request #51 from HideyoshiNakazone/chore/adds-pyright-config
chore: adds pyright config to project
2025-09-15 13:55:26 -03:00
f0192ee6d3 chore: adds pyright config to project 2025-09-15 13:54:32 -03:00
Fred Sonnenwald
82feea0ab1 Fix string Field deprecation warning
(partial revert of fbbff0b)
2025-09-15 16:50:49 +01:00
4d5ac1c885 Merge pull request #49 from HideyoshiNakazone/fix/fixes-docs
fix: fixes docs build
2025-09-14 10:51:26 -03:00
92c174c189 fix: fixes docs build 2025-09-14 10:49:53 -03:00
b1b5e71a81 Merge pull request #48 from HideyoshiNakazone/feature/explicit-exception-type
feat: more pythonic error parent class
2025-09-14 01:42:11 -03:00
156c825a67 feat: more pythonic error parent class 2025-09-14 01:40:59 -03:00
b4954c3b2e Merge pull request #47 from HideyoshiNakazone/feature/explicit-exception-type
Feature/explicit exception type
2025-09-14 01:13:27 -03:00
7f44e84bce feat: updates outdated docs for exceptions 2025-09-14 01:12:43 -03:00
8c6a04bbdf feat: adds simple tests for internal exceptions 2025-09-14 01:09:48 -03:00
e31002af32 feat: fixes tests to validate the type of exception thrown 2025-09-14 00:47:24 -03:00
30290771b1 feat: alters all standart errors and messages for more specific errors 2025-09-14 00:10:33 -03:00
f4d84d2749 feat: better exceptions for GenericTypeParser and AllOfTypeParser 2025-09-13 21:11:11 -03:00
e61d48881f feat: initial implementation of explicit exception types 2025-09-13 20:43:30 -03:00
f5ad857326 Merge pull request #46 from HideyoshiNakazone/feature/better-internal-typing
Better Internat Static Typing
2025-09-13 19:49:17 -03:00
e45086e29e feat: adds static type check to ci/cd 2025-09-13 19:48:17 -03:00
c1f04606ad fix: removes unecessary check 2025-09-13 19:36:53 -03:00
5eb086bafd Better Internat Static Typing 2025-09-13 00:16:41 -03:00
5c30e752e3 Merge pull request #45 from HideyoshiNakazone/chore/fixes-license-pyproject
chore: fixes license in pyproject - no change was made
2025-09-12 11:36:55 -03:00
53418f2b2b chore: fixes license in pyproject - no change was made 2025-09-12 11:36:11 -03:00
002b75c53a Merge pull request #44 from h0rv/feature/add-py-typed-support
feat: Add py.typed marker file for proper typing support
2025-09-12 10:23:13 -03:00
Robby
1167b8a540 feat: Add py.typed marker file for proper typing support
- Add py.typed marker file to jambo package directory
- Enable static type checkers to recognize and use type annotations from the library

This allows IDEs and tools like mypy, pyright to properly type-check code using this library.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 13:59:39 -04:00
3992057c95 Merge pull request #43 from HideyoshiNakazone/maintenance/format-lint-code
(improvement): Formats and Lints Code - Minor Changes
2025-08-20 01:13:25 -03:00
71380073e4 (improvement): Formats and Lints Code - Minor Changes 2025-08-20 01:12:56 -03:00
45 changed files with 875 additions and 320 deletions

View File

@@ -44,6 +44,9 @@ jobs:
uv run poe tests
uv run poe tests-report
- name: Static type check
run: uv run poe type-check
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:

View File

@@ -18,6 +18,7 @@ extensions = [
"sphinx.ext.viewcode",
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx_autodoc_typehints", # <-- needed
]
@@ -35,3 +36,6 @@ html_static_path = ["_static"]
# -- Options for autodoc -----------------------------------------------------
add_module_names = False
python_use_unqualified_type_names = True
autodoc_typehints = "both"

View File

@@ -0,0 +1,37 @@
jambo.exceptions package
========================
Submodules
----------
jambo.exceptions.internal\_assertion\_exception module
------------------------------------------------------
.. automodule:: jambo.exceptions.internal_assertion_exception
:members:
:show-inheritance:
:undoc-members:
jambo.exceptions.invalid\_schema\_exception module
--------------------------------------------------
.. automodule:: jambo.exceptions.invalid_schema_exception
:members:
:show-inheritance:
:undoc-members:
jambo.exceptions.unsupported\_schema\_exception module
------------------------------------------------------
.. automodule:: jambo.exceptions.unsupported_schema_exception
:members:
:show-inheritance:
:undoc-members:
Module contents
---------------
.. automodule:: jambo.exceptions
:members:
:show-inheritance:
:undoc-members:

View File

@@ -36,6 +36,22 @@ jambo.parser.boolean\_type\_parser module
:show-inheritance:
:undoc-members:
jambo.parser.const\_type\_parser module
---------------------------------------
.. automodule:: jambo.parser.const_type_parser
:members:
:show-inheritance:
:undoc-members:
jambo.parser.enum\_type\_parser module
--------------------------------------
.. automodule:: jambo.parser.enum_type_parser
:members:
:show-inheritance:
:undoc-members:
jambo.parser.float\_type\_parser module
---------------------------------------
@@ -52,6 +68,14 @@ jambo.parser.int\_type\_parser module
:show-inheritance:
:undoc-members:
jambo.parser.null\_type\_parser module
--------------------------------------
.. automodule:: jambo.parser.null_type_parser
:members:
:show-inheritance:
:undoc-members:
jambo.parser.object\_type\_parser module
----------------------------------------
@@ -60,6 +84,14 @@ jambo.parser.object\_type\_parser module
:show-inheritance:
:undoc-members:
jambo.parser.oneof\_type\_parser module
---------------------------------------
.. automodule:: jambo.parser.oneof_type_parser
:members:
:show-inheritance:
:undoc-members:
jambo.parser.ref\_type\_parser module
-------------------------------------

View File

@@ -7,6 +7,7 @@ Subpackages
.. toctree::
:maxdepth: 4
jambo.exceptions
jambo.parser
jambo.types

View File

@@ -0,0 +1,10 @@
from .internal_assertion_exception import InternalAssertionException
from .invalid_schema_exception import InvalidSchemaException
from .unsupported_schema_exception import UnsupportedSchemaException
__all__ = [
"InternalAssertionException",
"InvalidSchemaException",
"UnsupportedSchemaException",
]

View File

@@ -0,0 +1,16 @@
class InternalAssertionException(RuntimeError):
"""Exception raised for internal assertions."""
def __init__(
self,
message: str,
) -> None:
# Normalize message by stripping redundant prefix if present
message = message.removeprefix("Internal Assertion Failed: ")
super().__init__(message)
def __str__(self) -> str:
return (
f"Internal Assertion Failed: {super().__str__()}\n"
"This is likely a bug in Jambo. Please report it at"
)

View File

@@ -0,0 +1,27 @@
from typing_extensions import Optional
class InvalidSchemaException(ValueError):
"""Exception raised for invalid JSON schemas."""
def __init__(
self,
message: str,
invalid_field: Optional[str] = None,
cause: Optional[BaseException] = None,
) -> None:
# Normalize message by stripping redundant prefix if present
message = message.removeprefix("Invalid JSON Schema: ")
self.invalid_field = invalid_field
self.cause = cause
super().__init__(message)
def __str__(self) -> str:
base_msg = f"Invalid JSON Schema: {super().__str__()}"
if self.invalid_field:
return f"{base_msg} (invalid field: {self.invalid_field})"
if self.cause:
return (
f"{base_msg} (caused by {self.cause.__class__.__name__}: {self.cause})"
)
return base_msg

View File

@@ -0,0 +1,23 @@
from typing_extensions import Optional
class UnsupportedSchemaException(ValueError):
"""Exception raised for unsupported JSON schemas."""
def __init__(
self,
message: str,
unsupported_field: Optional[str] = None,
cause: Optional[BaseException] = None,
) -> None:
# Normalize message by stripping redundant prefix if present
message = message.removeprefix("Unsupported JSON Schema: ")
self.unsupported_field = unsupported_field
self.cause = cause
super().__init__(message)
def __str__(self) -> str:
base_msg = f"Unsupported JSON Schema: {super().__str__()}"
if self.unsupported_field:
return f"{base_msg} (unsupported field: {self.unsupported_field})"
return base_msg

View File

@@ -1,16 +1,17 @@
from jambo.types.type_parser_options import TypeParserOptions
from jambo.exceptions import InvalidSchemaException
from jambo.types.type_parser_options import JSONSchema, TypeParserOptions
from pydantic import Field, TypeAdapter
from typing_extensions import Annotated, Any, Generic, Self, TypeVar, Unpack
from typing_extensions import Annotated, Any, ClassVar, Generic, Self, TypeVar, Unpack
from abc import ABC, abstractmethod
T = TypeVar("T")
T = TypeVar("T", bound=type)
class GenericTypeParser(ABC, Generic[T]):
json_schema_type: str = None
json_schema_type: ClassVar[str]
type_mappings: dict[str, str] = {}
@@ -21,7 +22,7 @@ class GenericTypeParser(ABC, Generic[T]):
@abstractmethod
def from_properties_impl(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
) -> tuple[T, dict]:
"""
Abstract method to convert properties to a type and its fields properties.
@@ -32,7 +33,7 @@ class GenericTypeParser(ABC, Generic[T]):
"""
def from_properties(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
) -> tuple[T, dict]:
"""
Converts properties to a type and its fields properties.
@@ -46,15 +47,15 @@ class GenericTypeParser(ABC, Generic[T]):
)
if not self._validate_default(parsed_type, parsed_properties):
raise ValueError(
f"Default value {properties.get('default')} is not valid for type {parsed_type.__name__}"
raise InvalidSchemaException(
"Default value is not valid", invalid_field=name
)
return parsed_type, parsed_properties
@classmethod
def type_from_properties(
cls, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
cls, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
) -> tuple[type, dict]:
"""
Factory method to fetch the appropriate type parser based on properties
@@ -69,17 +70,19 @@ class GenericTypeParser(ABC, Generic[T]):
return parser().from_properties(name=name, properties=properties, **kwargs)
@classmethod
def _get_impl(cls, properties: dict[str, Any]) -> type[Self]:
def _get_impl(cls, properties: JSONSchema) -> type[Self]:
for subcls in cls.__subclasses__():
schema_type, schema_value = subcls._get_schema_type()
if schema_type not in properties:
continue
if schema_value is None or schema_value == properties[schema_type]:
if schema_value is None or schema_value == properties[schema_type]: # type: ignore
return subcls
raise ValueError("Unknown type")
raise InvalidSchemaException(
"No suitable type parser found", invalid_field=str(properties)
)
@classmethod
def _get_schema_type(cls) -> tuple[str, str | None]:
@@ -108,7 +111,7 @@ class GenericTypeParser(ABC, Generic[T]):
}
@staticmethod
def _validate_default(field_type: type, field_prop: dict) -> bool:
def _validate_default(field_type: T, field_prop: dict) -> bool:
value = field_prop.get("default")
if value is None and field_prop.get("default_factory") is not None:
@@ -118,7 +121,7 @@ class GenericTypeParser(ABC, Generic[T]):
return True
try:
field = Annotated[field_type, Field(**field_prop)]
field = Annotated[field_type, Field(**field_prop)] # type: ignore
TypeAdapter(field).validate_python(value)
except Exception as _:
return False

View File

@@ -1,7 +1,9 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser._type_parser import GenericTypeParser
from jambo.types.json_schema_type import JSONSchema
from jambo.types.type_parser_options import TypeParserOptions
from typing_extensions import Any, Unpack
from typing_extensions import Unpack
class AllOfTypeParser(GenericTypeParser):
@@ -10,7 +12,7 @@ class AllOfTypeParser(GenericTypeParser):
json_schema_type = "allOf"
def from_properties_impl(
self, name, properties, **kwargs: Unpack[TypeParserOptions]
self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
):
sub_properties = properties.get("allOf", [])
@@ -29,32 +31,39 @@ class AllOfTypeParser(GenericTypeParser):
@staticmethod
def _get_type_parser(
sub_properties: list[dict[str, Any]],
sub_properties: list[JSONSchema],
) -> type[GenericTypeParser]:
if not sub_properties:
raise ValueError("Invalid JSON Schema: 'allOf' is empty.")
raise InvalidSchemaException(
"'allOf' must contain at least one schema", invalid_field="allOf"
)
parsers = set(
parsers: set[type[GenericTypeParser]] = set(
GenericTypeParser._get_impl(sub_property) for sub_property in sub_properties
)
if len(parsers) != 1:
raise ValueError("Invalid JSON Schema: allOf types do not match.")
raise InvalidSchemaException(
"All sub-schemas in 'allOf' must resolve to the same type",
invalid_field="allOf",
)
return parsers.pop()
@staticmethod
def _rebuild_properties_from_subproperties(
sub_properties: list[dict[str, Any]],
) -> dict[str, Any]:
properties = {}
sub_properties: list[JSONSchema],
) -> JSONSchema:
properties: JSONSchema = {}
for subProperty in sub_properties:
for name, prop in subProperty.items():
if name not in properties:
properties[name] = prop
properties[name] = prop # type: ignore
else:
# Merge properties if they exist in both sub-properties
properties[name] = AllOfTypeParser._validate_prop(
name, properties[name], prop
properties[name] = AllOfTypeParser._validate_prop( # type: ignore
name,
properties[name], # type: ignore
prop,
)
return properties
@@ -65,8 +74,8 @@ class AllOfTypeParser(GenericTypeParser):
if prop_name == "default":
if old_value != new_value:
raise ValueError(
f"Invalid JSON Schema: conflicting defaults for '{prop_name}'"
raise InvalidSchemaException(
f"Conflicting defaults for '{prop_name}'", invalid_field=prop_name
)
return old_value

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions
@@ -14,10 +15,15 @@ class AnyOfTypeParser(GenericTypeParser):
self, name, properties, **kwargs: Unpack[TypeParserOptions]
):
if "anyOf" not in properties:
raise ValueError(f"Invalid JSON Schema: {properties}")
raise InvalidSchemaException(
f"AnyOf type {name} must have 'anyOf' property defined.",
invalid_field="anyOf",
)
if not isinstance(properties["anyOf"], list):
raise ValueError(f"Invalid JSON Schema: {properties['anyOf']}")
raise InvalidSchemaException(
"AnyOf must be a list of types.", invalid_field="anyOf"
)
mapped_properties = self.mappings_properties_builder(properties, **kwargs)

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions
@@ -26,8 +27,15 @@ class ArrayTypeParser(GenericTypeParser):
):
item_properties = kwargs.copy()
item_properties["required"] = True
if (items := properties.get("items")) is None:
raise InvalidSchemaException(
f"Array type {name} must have 'items' property defined.",
invalid_field="items",
)
_item_type, _item_args = GenericTypeParser.type_from_properties(
name, properties["items"], **item_properties
name, items, **item_properties
)
wrapper_type = set if properties.get("uniqueItems", False) else list
@@ -47,8 +55,9 @@ class ArrayTypeParser(GenericTypeParser):
return lambda: None
if not isinstance(default_list, Iterable):
raise ValueError(
f"Default value for array must be an iterable, got {type(default_list)}"
raise InvalidSchemaException(
f"Default value for array must be an iterable, got {type(default_list)}",
invalid_field="default",
)
return lambda: copy.deepcopy(wrapper_type(default_list))

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions
@@ -20,6 +21,9 @@ class BooleanTypeParser(GenericTypeParser):
default_value = properties.get("default")
if default_value is not None and not isinstance(default_value, bool):
raise ValueError(f"Default value for {name} must be a boolean.")
raise InvalidSchemaException(
f"Default value for {name} must be a boolean.",
invalid_field="default",
)
return bool, mapped_properties

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser._type_parser import GenericTypeParser
from jambo.types.json_schema_type import JSONSchemaNativeTypes
from jambo.types.type_parser_options import TypeParserOptions
@@ -18,13 +19,17 @@ class ConstTypeParser(GenericTypeParser):
self, name, properties, **kwargs: Unpack[TypeParserOptions]
):
if "const" not in properties:
raise ValueError(f"Const type {name} must have 'const' property defined.")
raise InvalidSchemaException(
f"Const type {name} must have 'const' property defined.",
invalid_field="const",
)
const_value = properties["const"]
if not isinstance(const_value, JSONSchemaNativeTypes):
raise ValueError(
f"Const type {name} must have 'const' value of allowed types: {JSONSchemaNativeTypes}."
raise InvalidSchemaException(
f"Const type {name} must have 'const' value of allowed types: {JSONSchemaNativeTypes}.",
invalid_field="const",
)
const_type = self._build_const_type(const_value)

View File

@@ -1,6 +1,7 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser._type_parser import GenericTypeParser
from jambo.types.json_schema_type import JSONSchemaNativeTypes
from jambo.types.type_parser_options import TypeParserOptions
from jambo.types.type_parser_options import JSONSchema, TypeParserOptions
from typing_extensions import Unpack
@@ -11,30 +12,33 @@ class EnumTypeParser(GenericTypeParser):
json_schema_type = "enum"
def from_properties_impl(
self, name, properties, **kwargs: Unpack[TypeParserOptions]
self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
):
if "enum" not in properties:
raise ValueError(f"Enum type {name} must have 'enum' property defined.")
raise InvalidSchemaException(
f"Enum type {name} must have 'enum' property defined.",
invalid_field="enum",
)
enum_values = properties["enum"]
if not isinstance(enum_values, list):
raise ValueError(f"Enum type {name} must have 'enum' as a list of values.")
raise InvalidSchemaException(
f"Enum type {name} must have 'enum' as a list of values.",
invalid_field="enum",
)
if any(
not isinstance(value, JSONSchemaNativeTypes) for value in enum_values
):
raise ValueError(
f"Enum type {name} must have 'enum' values of allowed types: {JSONSchemaNativeTypes}."
if any(not isinstance(value, JSONSchemaNativeTypes) for value in enum_values):
raise InvalidSchemaException(
f"Enum type {name} must have 'enum' values of allowed types: {JSONSchemaNativeTypes}.",
invalid_field="enum",
)
# Create a new Enum type dynamically
enum_type = Enum(name, {str(value).upper(): value for value in enum_values})
enum_type = Enum(name, {str(value).upper(): value for value in enum_values}) # type: ignore
parsed_properties = self.mappings_properties_builder(properties, **kwargs)
if (
"default" in parsed_properties and parsed_properties["default"] is not None
):
if "default" in parsed_properties and parsed_properties["default"] is not None:
parsed_properties["default"] = enum_type(parsed_properties["default"])
return enum_type, parsed_properties

View File

@@ -1,8 +1,10 @@
from jambo.parser._type_parser import GenericTypeParser
from jambo.types.json_schema_type import JSONSchema
from jambo.types.type_parser_options import TypeParserOptions
from pydantic import BaseModel, ConfigDict, Field, create_model
from typing_extensions import Any, Unpack
from pydantic.fields import FieldInfo
from typing_extensions import Unpack
class ObjectTypeParser(GenericTypeParser):
@@ -11,7 +13,7 @@ class ObjectTypeParser(GenericTypeParser):
json_schema_type = "type:object"
def from_properties_impl(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
) -> tuple[type[BaseModel], dict]:
type_parsing = self.to_model(
name,
@@ -32,29 +34,29 @@ class ObjectTypeParser(GenericTypeParser):
def to_model(
cls,
name: str,
schema: dict[str, Any],
properties: dict[str, JSONSchema],
required_keys: list[str],
**kwargs: Unpack[TypeParserOptions],
) -> type[BaseModel]:
"""
Converts JSON Schema object properties to a Pydantic model.
:param name: The name of the model.
:param schema: The properties of the JSON Schema object.
:param properties: The properties of the JSON Schema object.
:param required_keys: List of required keys in the schema.
:return: A Pydantic model class.
"""
model_config = ConfigDict(validate_assignment=True)
fields = cls._parse_properties(schema, required_keys, **kwargs)
fields = cls._parse_properties(properties, required_keys, **kwargs)
return create_model(name, __config__=model_config, **fields)
return create_model(name, __config__=model_config, **fields) # type: ignore
@classmethod
def _parse_properties(
cls,
properties: dict[str, Any],
properties: dict[str, JSONSchema],
required_keys: list[str],
**kwargs: Unpack[TypeParserOptions],
) -> dict[str, tuple[type, Field]]:
) -> dict[str, tuple[type, FieldInfo]]:
required_keys = required_keys or []
fields = {}
@@ -63,7 +65,9 @@ class ObjectTypeParser(GenericTypeParser):
sub_property["required"] = name in required_keys
parsed_type, parsed_properties = GenericTypeParser.type_from_properties(
name, prop, **sub_property
name,
prop,
**sub_property, # type: ignore
)
fields[name] = (parsed_type, Field(**parsed_properties))

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions
@@ -5,6 +6,9 @@ from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, ValidationE
from typing_extensions import Annotated, Any, Union, Unpack, get_args
Annotation = Annotated[Any, ...]
class OneOfTypeParser(GenericTypeParser):
mapped_type = Union
@@ -14,10 +18,14 @@ class OneOfTypeParser(GenericTypeParser):
self, name, properties, **kwargs: Unpack[TypeParserOptions]
):
if "oneOf" not in properties:
raise ValueError(f"Invalid JSON Schema: {properties}")
raise InvalidSchemaException(
f"Invalid JSON Schema: {properties}", invalid_field="oneOf"
)
if not isinstance(properties["oneOf"], list) or len(properties["oneOf"]) == 0:
raise ValueError(f"Invalid JSON Schema: {properties['oneOf']}")
raise InvalidSchemaException(
f"Invalid JSON Schema: {properties['oneOf']}", invalid_field="oneOf"
)
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
@@ -49,13 +57,15 @@ class OneOfTypeParser(GenericTypeParser):
@staticmethod
def _build_type_one_of_with_discriminator(
subfield_types: list[Annotated], discriminator_prop: dict
) -> Annotated:
subfield_types: list[Annotation], discriminator_prop: dict
) -> Annotation:
"""
Build a type with a discriminator.
"""
if not isinstance(discriminator_prop, dict):
raise ValueError("Discriminator must be a dictionary")
raise InvalidSchemaException(
"Discriminator must be a dictionary", invalid_field="discriminator"
)
for field in subfield_types:
field_type, field_info = get_args(field)
@@ -63,18 +73,22 @@ class OneOfTypeParser(GenericTypeParser):
if issubclass(field_type, BaseModel):
continue
raise ValueError(
"When using a discriminator, all subfield types must be of type 'object'."
raise InvalidSchemaException(
"When using a discriminator, all subfield types must be of type 'object'.",
invalid_field="discriminator",
)
property_name = discriminator_prop.get("propertyName")
if property_name is None or not isinstance(property_name, str):
raise ValueError("Discriminator must have a 'propertyName' key")
raise InvalidSchemaException(
"Discriminator must have a 'propertyName' key",
invalid_field="propertyName",
)
return Annotated[Union[(*subfield_types,)], Field(discriminator=property_name)]
@staticmethod
def _build_type_one_of_with_func(subfield_types: list[Annotated]) -> Annotated:
def _build_type_one_of_with_func(subfield_types: list[Annotation]) -> Annotation:
"""
Build a type with a validation function for the oneOf constraint.
"""

View File

@@ -1,10 +1,12 @@
from jambo.exceptions import InternalAssertionException, InvalidSchemaException
from jambo.parser import GenericTypeParser
from jambo.types.json_schema_type import JSONSchema
from jambo.types.type_parser_options import TypeParserOptions
from typing_extensions import Any, ForwardRef, Literal, TypeVar, Union, Unpack
from typing_extensions import ForwardRef, Literal, Union, Unpack
RefType = TypeVar("RefType", bound=Union[type, ForwardRef])
RefType = Union[type, ForwardRef]
RefStrategy = Literal["forward_ref", "def_ref"]
@@ -13,21 +15,22 @@ class RefTypeParser(GenericTypeParser):
json_schema_type = "$ref"
def from_properties_impl(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
) -> tuple[RefType, dict]:
if "$ref" not in properties:
raise ValueError(f"RefTypeParser: Missing $ref in properties for {name}")
raise InvalidSchemaException(
f"Missing $ref in properties for {name}", invalid_field="$ref"
)
context = kwargs.get("context")
if context is None:
raise RuntimeError(
f"RefTypeParser: Missing `content` in properties for {name}"
if kwargs.get("context") is None:
raise InternalAssertionException(
"`context` must be provided in kwargs for RefTypeParser"
)
ref_cache = kwargs.get("ref_cache")
if ref_cache is None:
raise RuntimeError(
f"RefTypeParser: Missing `ref_cache` in properties for {name}"
raise InternalAssertionException(
"`ref_cache` must be provided in kwargs for RefTypeParser"
)
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
@@ -41,19 +44,19 @@ class RefTypeParser(GenericTypeParser):
# If the reference is either processing or already cached
return ref_state, mapped_properties
ref_cache[ref_name] = self._parse_from_strategy(
ref_strategy, ref_name, ref_property, **kwargs
)
ref = self._parse_from_strategy(ref_strategy, ref_name, ref_property, **kwargs)
ref_cache[ref_name] = ref
return ref_cache[ref_name], mapped_properties
return ref, mapped_properties
def _parse_from_strategy(
self,
ref_strategy: RefStrategy,
ref_name: str,
ref_property: dict[str, Any],
ref_property: JSONSchema,
**kwargs: Unpack[TypeParserOptions],
):
) -> RefType:
mapped_type: RefType
match ref_strategy:
case "forward_ref":
mapped_type = ForwardRef(ref_name)
@@ -62,14 +65,14 @@ class RefTypeParser(GenericTypeParser):
ref_name, ref_property, **kwargs
)
case _:
raise ValueError(
f"RefTypeParser: Unsupported $ref {ref_property['$ref']}"
raise InvalidSchemaException(
f"Unsupported $ref {ref_property['$ref']}", invalid_field="$ref"
)
return mapped_type
def _get_ref_from_cache(
self, ref_name: str, ref_cache: dict[str, type]
self, ref_name: str, ref_cache: dict[str, ForwardRef | type | None]
) -> RefType | type | None:
try:
ref_state = ref_cache[ref_name]
@@ -84,42 +87,48 @@ class RefTypeParser(GenericTypeParser):
# If the reference is not in the cache, we will set it to None
ref_cache[ref_name] = None
return None
def _examine_ref_strategy(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
) -> tuple[RefStrategy, str, dict] | None:
if properties["$ref"] == "#":
self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
) -> tuple[RefStrategy, str, JSONSchema]:
if properties.get("$ref") == "#":
ref_name = kwargs["context"].get("title")
if ref_name is None:
raise ValueError(
"RefTypeParser: Missing title in properties for $ref of Root Reference"
raise InvalidSchemaException(
"Missing title in properties for $ref of Root Reference",
invalid_field="title",
)
return "forward_ref", ref_name, {}
if properties["$ref"].startswith("#/$defs/"):
if properties.get("$ref", "").startswith("#/$defs/"):
target_name, target_property = self._extract_target_ref(
name, properties, **kwargs
)
return "def_ref", target_name, target_property
raise ValueError(
"RefTypeParser: Only Root and $defs references are supported at the moment"
raise InvalidSchemaException(
"Only Root and $defs references are supported at the moment",
invalid_field="$ref",
)
def _extract_target_ref(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions]
) -> tuple[str, dict]:
self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
) -> tuple[str, JSONSchema]:
target_name = None
target_property = kwargs["context"]
for prop_name in properties["$ref"].split("/")[1:]:
if prop_name not in target_property:
raise ValueError(
f"RefTypeParser: Missing {prop_name} in"
" properties for $ref {properties['$ref']}"
raise InvalidSchemaException(
f"Missing {prop_name} in properties for $ref {properties['$ref']}",
invalid_field=prop_name,
)
target_name = prop_name
target_property = target_property[prop_name]
target_property = target_property[prop_name] # type: ignore
if target_name is None or target_property is None:
raise ValueError(f"RefTypeParser: Invalid $ref {properties['$ref']}")
if not isinstance(target_name, str) or target_property is None:
raise InvalidSchemaException(
f"Invalid $ref {properties['$ref']}", invalid_field="$ref"
)
return target_name, target_property

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions
@@ -18,23 +19,22 @@ class StringTypeParser(GenericTypeParser):
"maxLength": "max_length",
"minLength": "min_length",
"pattern": "pattern",
"format": "format",
}
format_type_mapping = {
# 7.3.1. Dates, Times, and Duration
# [7.3.1](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.1). Dates, Times, and Duration
"date": date,
"time": time,
"date-time": datetime,
"duration": timedelta,
# 7.3.2. Email Addresses
# [7.3.2](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.2). Email Addresses
"email": EmailStr,
# 7.3.3. Hostnames
# [7.3.3](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.3). Hostnames
"hostname": str,
# 7.3.4. IP Addresses
# [7.3.4](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.4). IP Addresses
"ipv4": IPv4Address,
"ipv6": IPv6Address,
# 7.3.5. Resource Identifiers
# [7.3.5](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.5). Resource Identifiers
"uri": AnyUrl,
# "iri" # Not supported by pydantic and currently not supported by jambo
"uuid": UUID,
@@ -54,10 +54,16 @@ class StringTypeParser(GenericTypeParser):
return str, mapped_properties
if format_type not in self.format_type_mapping:
raise ValueError(f"Unsupported string format: {format_type}")
raise InvalidSchemaException(
f"Unsupported string format: {format_type}", invalid_field="format"
)
mapped_type = self.format_type_mapping[format_type]
if format_type in self.format_pattern_mapping:
mapped_properties["pattern"] = self.format_pattern_mapping[format_type]
if "json_schema_extra" not in mapped_properties:
mapped_properties["json_schema_extra"] = {}
mapped_properties["json_schema_extra"]["format"] = format_type
return mapped_type, mapped_properties

0
jambo/py.typed Normal file
View File

View File

@@ -1,5 +1,6 @@
from jambo.exceptions import InvalidSchemaException, UnsupportedSchemaException
from jambo.parser import ObjectTypeParser, RefTypeParser
from jambo.types.json_schema_type import JSONSchema
from jambo.types import JSONSchema
from jsonschema.exceptions import SchemaError
from jsonschema.validators import validator_for
@@ -25,12 +26,16 @@ class SchemaConverter:
try:
validator = validator_for(schema)
validator.check_schema(schema)
except SchemaError as e:
raise ValueError(f"Invalid JSON Schema: {e}")
validator.check_schema(schema) # type: ignore
except SchemaError as err:
raise InvalidSchemaException(
"Validation of JSON Schema failed.", cause=err
) from err
if "title" not in schema:
raise ValueError("JSON Schema must have a title.")
raise InvalidSchemaException(
"Schema must have a title.", invalid_field="title"
)
schema_type = SchemaConverter._get_schema_type(schema)
@@ -38,10 +43,11 @@ class SchemaConverter:
case "object":
return ObjectTypeParser.to_model(
schema["title"],
schema["properties"],
schema.get("properties", {}),
schema.get("required", []),
context=schema,
ref_cache=dict(),
required=True,
)
case "$ref":
@@ -50,13 +56,20 @@ class SchemaConverter:
schema,
context=schema,
ref_cache=dict(),
required=True,
)
return parsed_model
case _:
raise TypeError(f"Unsupported schema type: {schema_type}")
unsupported_type = (
f"type:{schema_type}" if schema_type else "missing type"
)
raise UnsupportedSchemaException(
"Only object and $ref schema types are supported.",
unsupported_field=unsupported_type,
)
@staticmethod
def _get_schema_type(schema: JSONSchema) -> str:
def _get_schema_type(schema: JSONSchema) -> str | None:
"""
Returns the type of the schema.
:param schema: The JSON Schema to check.
@@ -65,4 +78,4 @@ class SchemaConverter:
if "$ref" in schema:
return "$ref"
return schema.get("type", "undefined")
return schema.get("type")

View File

@@ -0,0 +1,16 @@
from .json_schema_type import (
JSONSchema,
JSONSchemaNativeTypes,
JSONSchemaType,
JSONType,
)
from .type_parser_options import TypeParserOptions
__all__ = [
"JSONSchemaType",
"JSONSchemaNativeTypes",
"JSONType",
"JSONSchema",
"TypeParserOptions",
]

View File

@@ -1,93 +1,80 @@
from typing_extensions import Dict, List, Literal, TypedDict, Union
from __future__ import annotations
from typing_extensions import (
Dict,
List,
Literal,
TypedDict,
Union,
)
from types import NoneType
# Primitive JSON types
JSONSchemaType = Literal[
"string", "number", "integer", "boolean", "object", "array", "null"
]
JSONSchemaNativeTypes: tuple[type, ...] = (
str,
int,
float,
int,
bool,
list,
set,
NoneType,
)
JSONType = Union[str, int, float, bool, None, Dict[str, "JSONType"], List["JSONType"]]
class JSONSchema(TypedDict, total=False):
# Basic metadata
title: str
description: str
default: JSONType
examples: List[JSONType]
# Type definitions
type: Union[JSONSchemaType, List[JSONSchemaType]]
# Object-specific keywords
properties: Dict[str, "JSONSchema"]
required: List[str]
additionalProperties: Union[bool, "JSONSchema"]
minProperties: int
maxProperties: int
patternProperties: Dict[str, "JSONSchema"]
dependencies: Dict[str, Union[List[str], "JSONSchema"]]
# Array-specific keywords
items: Union["JSONSchema", List["JSONSchema"]]
additionalItems: Union[bool, "JSONSchema"]
minItems: int
maxItems: int
uniqueItems: bool
# String-specific keywords
minLength: int
maxLength: int
pattern: str
format: str
# Number-specific keywords
minimum: float
maximum: float
exclusiveMinimum: float
exclusiveMaximum: float
multipleOf: float
# Enum and const
enum: List[JSONType]
const: JSONType
# Conditionals
if_: "JSONSchema" # 'if' is a reserved word in Python
then: "JSONSchema"
else_: "JSONSchema" # 'else' is also a reserved word
# Combination keywords
allOf: List["JSONSchema"]
anyOf: List["JSONSchema"]
oneOf: List["JSONSchema"]
not_: "JSONSchema" # 'not' is a reserved word
# Fix forward references
JSONSchema.__annotations__["properties"] = Dict[str, JSONSchema]
JSONSchema.__annotations__["items"] = Union[JSONSchema, List[JSONSchema]]
JSONSchema.__annotations__["additionalItems"] = Union[bool, JSONSchema]
JSONSchema.__annotations__["additionalProperties"] = Union[bool, JSONSchema]
JSONSchema.__annotations__["patternProperties"] = Dict[str, JSONSchema]
JSONSchema.__annotations__["dependencies"] = Dict[str, Union[List[str], JSONSchema]]
JSONSchema.__annotations__["if_"] = JSONSchema
JSONSchema.__annotations__["then"] = JSONSchema
JSONSchema.__annotations__["else_"] = JSONSchema
JSONSchema.__annotations__["allOf"] = List[JSONSchema]
JSONSchema.__annotations__["anyOf"] = List[JSONSchema]
JSONSchema.__annotations__["oneOf"] = List[JSONSchema]
JSONSchema.__annotations__["not_"] = JSONSchema
# Dynamically define TypedDict with JSON Schema keywords
JSONSchema = TypedDict(
"JSONSchema",
{
"$id": str,
"$schema": str,
"$ref": str,
"$anchor": str,
"$comment": str,
"$defs": Dict[str, "JSONSchema"],
"title": str,
"description": str,
"default": JSONType,
"examples": List[JSONType],
"type": JSONSchemaType,
"enum": List[JSONType],
"const": JSONType,
"properties": Dict[str, "JSONSchema"],
"patternProperties": Dict[str, "JSONSchema"],
"additionalProperties": Union[bool, "JSONSchema"],
"required": List[str],
"minProperties": int,
"maxProperties": int,
"dependencies": Dict[str, Union[List[str], "JSONSchema"]],
"items": "JSONSchema",
"prefixItems": List["JSONSchema"],
"additionalItems": Union[bool, "JSONSchema"],
"contains": "JSONSchema",
"minItems": int,
"maxItems": int,
"uniqueItems": bool,
"minLength": int,
"maxLength": int,
"pattern": str,
"format": str,
"minimum": float,
"maximum": float,
"exclusiveMinimum": Union[bool, float],
"exclusiveMaximum": Union[bool, float],
"multipleOf": float,
"if": "JSONSchema",
"then": "JSONSchema",
"else": "JSONSchema",
"allOf": List["JSONSchema"],
"anyOf": List["JSONSchema"],
"oneOf": List["JSONSchema"],
"not": "JSONSchema",
},
total=False, # all fields optional
)

View File

@@ -1,9 +1,9 @@
from jambo.types.json_schema_type import JSONSchema
from typing_extensions import TypedDict
from typing_extensions import ForwardRef, TypedDict
class TypeParserOptions(TypedDict):
required: bool
context: JSONSchema
ref_cache: dict[str, type]
ref_cache: dict[str, ForwardRef | type | None]

View File

@@ -18,7 +18,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
license = { file = "LICENSE" }
license = "MIT"
readme = "README.md"
# Project Dependencies
@@ -31,12 +31,15 @@ dependencies = [
[dependency-groups]
dev = [
"coverage>=7.8.0",
"mypy>=1.18.1",
"poethepoet>=0.33.1",
"pre-commit>=4.2.0",
"ruff>=0.11.4",
"sphinx>=8.1.3",
"sphinx-autobuild>=2024.10.3",
"sphinx-autodoc-typehints>=3.0.1",
"sphinx-rtd-theme>=3.0.2",
"types-jsonschema>=4.25.1.20250822",
]
@@ -50,6 +53,7 @@ repository = "https://github.com/HideyoshiNakazone/jambo.git"
create-hooks = "bash .githooks/set-hooks.sh"
tests = "python -m coverage run -m unittest discover -v"
tests-report = "python -m coverage xml"
type-check = "mypy jambo"
serve-docs = "sphinx-autobuild docs/source docs/build"
# Build System
@@ -83,3 +87,8 @@ section-order=[
"standard-library",
]
lines-after-imports = 2
[tool.pyright]
venvPath = "."
venv = ".venv"

View File

View File

@@ -0,0 +1,21 @@
from jambo.exceptions.internal_assertion_exception import InternalAssertionException
from unittest import TestCase
class TestInternalAssertionException(TestCase):
def test_inheritance(self):
self.assertTrue(issubclass(InternalAssertionException, RuntimeError))
def test_message(self):
message = "This is an internal assertion error."
expected_message = (
f"Internal Assertion Failed: {message}\n"
"This is likely a bug in Jambo. Please report it at"
)
with self.assertRaises(InternalAssertionException) as ctx:
raise InternalAssertionException(message)
self.assertEqual(str(ctx.exception), expected_message)

View File

@@ -0,0 +1,44 @@
from jambo.exceptions.invalid_schema_exception import InvalidSchemaException
from unittest import TestCase
class TestInternalAssertionException(TestCase):
def test_inheritance(self):
self.assertTrue(issubclass(InvalidSchemaException, ValueError))
def test_message(self):
message = "This is an internal assertion error."
expected_message = f"Invalid JSON Schema: {message}"
with self.assertRaises(InvalidSchemaException) as ctx:
raise InvalidSchemaException(message)
self.assertEqual(str(ctx.exception), expected_message)
def test_invalid_field(self):
message = "This is an internal assertion error."
invalid_field = "testField"
expected_message = (
f"Invalid JSON Schema: {message} (invalid field: {invalid_field})"
)
with self.assertRaises(InvalidSchemaException) as ctx:
raise InvalidSchemaException(message, invalid_field=invalid_field)
self.assertEqual(str(ctx.exception), expected_message)
def test_cause(self):
message = "This is an internal assertion error."
cause = ValueError("Underlying cause")
expected_message = (
f"Invalid JSON Schema: {message} (caused by ValueError: Underlying cause)"
)
with self.assertRaises(InvalidSchemaException) as ctx:
raise InvalidSchemaException(message, cause=cause)
self.assertEqual(str(ctx.exception), expected_message)

View File

@@ -0,0 +1,31 @@
from jambo.exceptions.unsupported_schema_exception import UnsupportedSchemaException
from unittest import TestCase
class TestUnsupportedSchemaException(TestCase):
def test_inheritance(self):
self.assertTrue(issubclass(UnsupportedSchemaException, ValueError))
def test_message(self):
message = "This is an internal assertion error."
expected_message = f"Unsupported JSON Schema: {message}"
with self.assertRaises(UnsupportedSchemaException) as ctx:
raise UnsupportedSchemaException(message)
self.assertEqual(str(ctx.exception), expected_message)
def test_unsupported_field(self):
message = "This is an internal assertion error."
invalid_field = "testField"
expected_message = (
f"Unsupported JSON Schema: {message} (unsupported field: {invalid_field})"
)
with self.assertRaises(UnsupportedSchemaException) as ctx:
raise UnsupportedSchemaException(message, unsupported_field=invalid_field)
self.assertEqual(str(ctx.exception), expected_message)

View File

@@ -1,5 +1,8 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser.allof_type_parser import AllOfTypeParser
from pydantic import ValidationError
from unittest import TestCase
@@ -42,13 +45,13 @@ class TestAllOfTypeParser(TestCase):
"placeholder", properties
)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
type_parsing(name="John", age=101)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
type_parsing(name="", age=30)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
type_parsing(name="John Invalid", age=30)
obj = type_parsing(name="John", age=30)
@@ -87,10 +90,10 @@ class TestAllOfTypeParser(TestCase):
"placeholder", properties
)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
type_parsing(name="John")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
type_parsing(age=30)
obj = type_parsing(name="John", age=30)
@@ -154,7 +157,7 @@ class TestAllOfTypeParser(TestCase):
]
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
AllOfTypeParser().from_properties("placeholder", properties)
def test_all_of_invalid_type_not_present(self):
@@ -167,7 +170,7 @@ class TestAllOfTypeParser(TestCase):
]
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
AllOfTypeParser().from_properties("placeholder", properties)
def test_all_of_invalid_type_in_fields(self):
@@ -180,7 +183,7 @@ class TestAllOfTypeParser(TestCase):
]
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
AllOfTypeParser().from_properties("placeholder", properties)
def test_all_of_invalid_type_not_all_equal(self):
@@ -196,7 +199,7 @@ class TestAllOfTypeParser(TestCase):
]
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
AllOfTypeParser().from_properties("placeholder", properties)
def test_all_of_description_field(self):
@@ -304,5 +307,5 @@ class TestAllOfTypeParser(TestCase):
],
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
AllOfTypeParser().from_properties("placeholder", properties)

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser.anyof_type_parser import AnyOfTypeParser
from typing_extensions import Annotated, Union, get_args, get_origin
@@ -14,7 +15,7 @@ class TestAnyOfTypeParser(TestCase):
],
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
AnyOfTypeParser().from_properties("placeholder", properties)
def test_any_of_with_invalid_properties(self):
@@ -22,7 +23,7 @@ class TestAnyOfTypeParser(TestCase):
"anyOf": None,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
AnyOfTypeParser().from_properties("placeholder", properties)
def test_any_of_string_or_int(self):
@@ -95,5 +96,5 @@ class TestAnyOfTypeParser(TestCase):
"default": 3.14,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
AnyOfTypeParser().from_properties("placeholder", properties)

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser import ArrayTypeParser
from typing_extensions import get_args
@@ -18,6 +19,17 @@ class TestArrayTypeParser(TestCase):
self.assertEqual(type_parsing.__origin__, list)
self.assertEqual(element_type, str)
def test_array_parser_with_no_items(self):
parser = ArrayTypeParser()
properties = {
"default": ["a", "b", "c", "d"],
"maxItems": 3,
}
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_array_parser_with_options_unique(self):
parser = ArrayTypeParser()
@@ -67,7 +79,7 @@ class TestArrayTypeParser(TestCase):
properties = {"items": {"type": "string"}, "default": ["a", 1, "c"]}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_array_parser_with_invalid_default_type(self):
@@ -75,15 +87,15 @@ class TestArrayTypeParser(TestCase):
properties = {"items": {"type": "string"}, "default": 000}
with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties)
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties=properties)
def test_array_parser_with_invalid_default_min(self):
parser = ArrayTypeParser()
properties = {"items": {"type": "string"}, "default": ["a"], "minItems": 2}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_array_parser_with_invalid_default_max(self):
@@ -95,5 +107,5 @@ class TestArrayTypeParser(TestCase):
"maxItems": 3,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser import BooleanTypeParser
from unittest import TestCase
@@ -39,5 +40,5 @@ class TestBoolTypeParser(TestCase):
"default": "invalid",
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties_impl("placeholder", properties)

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser import ConstTypeParser
from typing_extensions import Annotated, Literal, get_args, get_origin
@@ -80,7 +81,7 @@ class TestConstTypeParser(TestCase):
expected_const_value = "United States of America"
properties = {"notConst": expected_const_value}
with self.assertRaises(ValueError) as context:
with self.assertRaises(InvalidSchemaException) as context:
parser.from_properties_impl("invalid_country", properties)
self.assertIn(
@@ -93,7 +94,7 @@ class TestConstTypeParser(TestCase):
properties = {"const": {}}
with self.assertRaises(ValueError) as context:
with self.assertRaises(InvalidSchemaException) as context:
parser.from_properties_impl("invalid_country", properties)
self.assertIn(

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser import EnumTypeParser
from enum import Enum
@@ -10,7 +11,7 @@ class TestEnumTypeParser(TestCase):
schema = {}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parsed_type, parsed_properties = parser.from_properties_impl(
"TestEnum",
schema,
@@ -23,7 +24,7 @@ class TestEnumTypeParser(TestCase):
"enum": "not_a_list",
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parsed_type, parsed_properties = parser.from_properties_impl(
"TestEnum",
schema,
@@ -86,5 +87,5 @@ class TestEnumTypeParser(TestCase):
"enum": ["value1", 42, dict()],
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties_impl("TestEnum", schema)

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser import FloatTypeParser
from unittest import TestCase
@@ -61,7 +62,7 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 0.5,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_float_parser_with_default_invalid_maximum(self):
@@ -75,7 +76,7 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 0.5,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_float_parser_with_default_invalid_minimum(self):
@@ -89,7 +90,7 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 0.5,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_float_parser_with_default_invalid_exclusive_maximum(self):
@@ -103,7 +104,7 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 0.5,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_float_parser_with_default_invalid_exclusive_minimum(self):
@@ -117,7 +118,7 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 0.5,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_float_parser_with_default_invalid_multiple(self):
@@ -131,5 +132,5 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 2.0,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser import IntTypeParser
from unittest import TestCase
@@ -61,7 +62,7 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_int_parser_with_default_invalid_maximum(self):
@@ -75,7 +76,7 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_int_parser_with_default_invalid_minimum(self):
@@ -89,7 +90,7 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_int_parser_with_default_invalid_exclusive_maximum(self):
@@ -103,7 +104,7 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_int_parser_with_default_invalid_exclusive_minimum(self):
@@ -117,7 +118,7 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_int_parser_with_default_invalid_multipleOf(self):
@@ -131,5 +132,5 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)

View File

@@ -1,12 +1,15 @@
from jambo import SchemaConverter
from jambo.exceptions import InvalidSchemaException
from jambo.parser.oneof_type_parser import OneOfTypeParser
from pydantic import ValidationError
from unittest import TestCase
class TestOneOfTypeParser(TestCase):
def test_oneof_raises_on_invalid_property(self):
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
OneOfTypeParser().from_properties_impl(
"test_field",
{
@@ -17,7 +20,18 @@ class TestOneOfTypeParser(TestCase):
ref_cache={},
)
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
OneOfTypeParser().from_properties_impl(
"test_field",
{
"oneOf": [], # should throw because oneOf must be a list with at least one item
},
required=True,
context={},
ref_cache={},
)
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(
{
"title": "Test",
@@ -71,13 +85,13 @@ class TestOneOfTypeParser(TestCase):
Model = SchemaConverter.build(schema)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(id=-5)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(id="invalid")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(id=123.45)
def test_oneof_with_conflicting_schemas(self):
@@ -103,11 +117,11 @@ class TestOneOfTypeParser(TestCase):
obj2 = Model(data=9)
self.assertEqual(obj2.data, 9)
with self.assertRaises(ValueError) as cm:
with self.assertRaises(ValidationError) as cm:
Model(data=6)
self.assertIn("matches multiple oneOf schemas", str(cm.exception))
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(data=5)
def test_oneof_with_objects(self):
@@ -147,7 +161,7 @@ class TestOneOfTypeParser(TestCase):
obj2 = Model(contact_info={"phone": "123-456-7890"})
self.assertEqual(obj2.contact_info.phone, "123-456-7890")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(contact_info={"email": "user@example.com", "phone": "123-456-7890"})
def test_oneof_with_discriminator_basic(self):
@@ -190,14 +204,14 @@ class TestOneOfTypeParser(TestCase):
self.assertEqual(dog.pet.type, "dog")
self.assertEqual(dog.pet.barks, False)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(pet={"type": "cat", "barks": True})
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(pet={"type": "bird", "flies": True})
def test_oneof_with_invalid_types(self):
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(
{
"title": "Pet",
@@ -301,13 +315,13 @@ class TestOneOfTypeParser(TestCase):
Model = SchemaConverter.build(schema)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(shape={"type": "triangle", "base": 5, "height": 3})
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(shape={"type": "circle", "side": 5})
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(shape={"radius": 5})
def test_oneof_missing_properties(self):
@@ -324,7 +338,7 @@ class TestOneOfTypeParser(TestCase):
},
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(schema)
def test_oneof_invalid_properties(self):
@@ -336,7 +350,7 @@ class TestOneOfTypeParser(TestCase):
},
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(schema)
def test_oneof_with_default_value(self):
@@ -373,12 +387,12 @@ class TestOneOfTypeParser(TestCase):
},
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(schema)
def test_oneof_discriminator_without_property_name(self):
# Should throw because the spec determines propertyName is required for discriminator
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(
{
"title": "Test",
@@ -409,7 +423,7 @@ class TestOneOfTypeParser(TestCase):
def test_oneof_discriminator_with_invalid_discriminator(self):
# Should throw because a valid discriminator is required
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(
{
"title": "Test",
@@ -465,8 +479,9 @@ class TestOneOfTypeParser(TestCase):
self.assertEqual(obj2.value, "very long string")
# Invalid: Medium string (matches BOTH schemas - violates oneOf)
with self.assertRaises(ValueError) as cm:
with self.assertRaises(ValidationError) as cm:
Model(value="hello") # 5 chars: matches maxLength=6 AND minLength=4
self.assertIn("matches multiple oneOf schemas", str(cm.exception))
def test_oneof_shapes_discriminator_from_docs(self):
@@ -515,5 +530,5 @@ class TestOneOfTypeParser(TestCase):
self.assertEqual(rectangle.shape.height, 20)
# Invalid: Wrong properties for the type
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(shape={"type": "circle", "width": 10})

View File

@@ -1,5 +1,8 @@
from jambo.exceptions import InternalAssertionException, InvalidSchemaException
from jambo.parser import ObjectTypeParser, RefTypeParser
from pydantic import ValidationError
from typing import ForwardRef
from unittest import TestCase
@@ -16,7 +19,7 @@ class TestRefTypeParser(TestCase):
"required": ["name", "age"],
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
RefTypeParser().from_properties(
"person",
properties,
@@ -40,7 +43,7 @@ class TestRefTypeParser(TestCase):
},
}
with self.assertRaises(RuntimeError):
with self.assertRaises(InternalAssertionException):
RefTypeParser().from_properties(
"person",
properties,
@@ -63,7 +66,7 @@ class TestRefTypeParser(TestCase):
},
}
with self.assertRaises(RuntimeError):
with self.assertRaises(InternalAssertionException):
RefTypeParser().from_properties(
"person",
properties,
@@ -77,7 +80,7 @@ class TestRefTypeParser(TestCase):
"$ref": "https://example.com/schemas/person.json",
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
RefTypeParser().from_properties(
"person",
properties,
@@ -110,7 +113,7 @@ class TestRefTypeParser(TestCase):
},
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
ObjectTypeParser().from_properties(
"person",
properties,
@@ -126,7 +129,7 @@ class TestRefTypeParser(TestCase):
"$defs": {},
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
RefTypeParser().from_properties(
"person",
properties,
@@ -142,7 +145,7 @@ class TestRefTypeParser(TestCase):
"$defs": {"person": None},
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
RefTypeParser().from_properties(
"person",
properties,
@@ -232,7 +235,7 @@ class TestRefTypeParser(TestCase):
"required": ["name", "age"],
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
ObjectTypeParser().from_properties(
"person",
properties,
@@ -264,7 +267,7 @@ class TestRefTypeParser(TestCase):
)
# checks if when created via FowardRef the model is validated correctly.
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(
name="John",
age=30,
@@ -421,7 +424,7 @@ class TestRefTypeParser(TestCase):
},
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
ref_strategy, ref_name, ref_property = RefTypeParser()._parse_from_strategy(
"invalid_strategy",
"person",

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser import StringTypeParser
from pydantic import AnyUrl, EmailStr
@@ -62,7 +63,7 @@ class TestStringTypeParser(TestCase):
"minLength": 5,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_string_parser_with_default_invalid_maxlength(self):
@@ -75,7 +76,7 @@ class TestStringTypeParser(TestCase):
"minLength": 1,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_string_parser_with_default_invalid_minlength(self):
@@ -88,7 +89,7 @@ class TestStringTypeParser(TestCase):
"minLength": 2,
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties)
def test_string_parser_with_email_format(self):
@@ -183,11 +184,12 @@ class TestStringTypeParser(TestCase):
"format": "unsupported-format",
}
with self.assertRaises(ValueError) as context:
with self.assertRaises(InvalidSchemaException) as context:
parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception), "Unsupported string format: unsupported-format"
str(context.exception),
"Invalid JSON Schema: Unsupported string format: unsupported-format (invalid field: format)",
)
def test_string_parser_with_date_format(self):

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser import StringTypeParser
from jambo.parser._type_parser import GenericTypeParser
@@ -17,5 +18,5 @@ class TestGenericTypeParser(TestCase):
StringTypeParser.json_schema_type = "type:string"
def test_get_impl_invalid_type(self):
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
GenericTypeParser._get_impl({"type": "invalid_type"})

View File

@@ -1,6 +1,7 @@
from jambo import SchemaConverter
from jambo.exceptions import InvalidSchemaException, UnsupportedSchemaException
from pydantic import AnyUrl, BaseModel
from pydantic import AnyUrl, BaseModel, ValidationError
from ipaddress import IPv4Address, IPv6Address
from unittest import TestCase
@@ -23,7 +24,21 @@ class TestSchemaConverter(TestCase):
},
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(schema)
def test_invalid_schema_type(self):
schema = {
"title": 1,
"description": "A person",
"type": 1,
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
},
}
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(schema)
def test_build_expects_title(self):
@@ -36,7 +51,7 @@ class TestSchemaConverter(TestCase):
},
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(schema)
def test_build_expects_object(self):
@@ -46,7 +61,7 @@ class TestSchemaConverter(TestCase):
"type": "string",
}
with self.assertRaises(TypeError):
with self.assertRaises(UnsupportedSchemaException):
SchemaConverter.build(schema)
def test_is_invalid_field(self):
@@ -62,7 +77,7 @@ class TestSchemaConverter(TestCase):
# 'required': ['name', 'age', 'is_active', 'friends', 'address'],
}
with self.assertRaises(ValueError) as context:
with self.assertRaises(InvalidSchemaException) as context:
SchemaConverter.build(schema)
self.assertTrue("Unknown type" in str(context.exception))
@@ -103,16 +118,16 @@ class TestSchemaConverter(TestCase):
self.assertEqual(model(name="John", age=30).name, "John")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(name=123, age=30, email="teste@hideyoshi.com")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(name="John Invalid", age=45, email="teste@hideyoshi.com")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(name="", age=45, email="teste@hideyoshi.com")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(name="John", age=45, email="hideyoshi.com")
def test_validation_integer(self):
@@ -134,10 +149,10 @@ class TestSchemaConverter(TestCase):
self.assertEqual(model(age=30).age, 30)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(age=-1)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(age=121)
def test_validation_float(self):
@@ -159,10 +174,10 @@ class TestSchemaConverter(TestCase):
self.assertEqual(model(age=30).age, 30.0)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(age=-1.0)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(age=121.0)
def test_validation_boolean(self):
@@ -205,10 +220,10 @@ class TestSchemaConverter(TestCase):
model(friends=["John", "Jane", "John"]).friends, {"John", "Jane"}
)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(friends=[])
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(friends=["John", "Jane", "Invalid"])
def test_validation_list_with_missing_items(self):
@@ -248,7 +263,7 @@ class TestSchemaConverter(TestCase):
}
)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model()
def test_validation_object(self):
@@ -276,7 +291,7 @@ class TestSchemaConverter(TestCase):
self.assertEqual(obj.address.street, "123 Main St")
self.assertEqual(obj.address.city, "Springfield")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model()
def test_default_for_string(self):
@@ -315,7 +330,7 @@ class TestSchemaConverter(TestCase):
"required": ["name"],
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(schema_max_length)
def test_default_for_list(self):
@@ -407,10 +422,10 @@ class TestSchemaConverter(TestCase):
self.assertEqual(obj.name, "J")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(name="John Invalid")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(name="")
def test_any_of(self):
@@ -436,13 +451,13 @@ class TestSchemaConverter(TestCase):
obj = Model(id="12345678901")
self.assertEqual(obj.id, "12345678901")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(id="")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(id="12345678901234567890")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(id=11)
def test_string_format_email(self):
@@ -451,9 +466,11 @@ class TestSchemaConverter(TestCase):
"type": "object",
"properties": {"email": {"type": "string", "format": "email"}},
}
model = SchemaConverter.build(schema)
self.assertEqual(model(email="test@example.com").email, "test@example.com")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(email="invalid-email")
def test_string_format_uri(self):
@@ -462,11 +479,13 @@ class TestSchemaConverter(TestCase):
"type": "object",
"properties": {"website": {"type": "string", "format": "uri"}},
}
model = SchemaConverter.build(schema)
self.assertEqual(
model(website="https://example.com").website, AnyUrl("https://example.com")
)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(website="invalid-uri")
def test_string_format_ipv4(self):
@@ -475,9 +494,11 @@ class TestSchemaConverter(TestCase):
"type": "object",
"properties": {"ip": {"type": "string", "format": "ipv4"}},
}
model = SchemaConverter.build(schema)
self.assertEqual(model(ip="192.168.1.1").ip, IPv4Address("192.168.1.1"))
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(ip="256.256.256.256")
def test_string_format_ipv6(self):
@@ -486,12 +507,14 @@ class TestSchemaConverter(TestCase):
"type": "object",
"properties": {"ip": {"type": "string", "format": "ipv6"}},
}
model = SchemaConverter.build(schema)
self.assertEqual(
model(ip="2001:0db8:85a3:0000:0000:8a2e:0370:7334").ip,
IPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(ip="invalid-ipv6")
def test_string_format_uuid(self):
@@ -500,6 +523,7 @@ class TestSchemaConverter(TestCase):
"type": "object",
"properties": {"id": {"type": "string", "format": "uuid"}},
}
model = SchemaConverter.build(schema)
self.assertEqual(
@@ -507,7 +531,7 @@ class TestSchemaConverter(TestCase):
UUID("123e4567-e89b-12d3-a456-426614174000"),
)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(id="invalid-uuid")
def test_string_format_hostname(self):
@@ -516,9 +540,11 @@ class TestSchemaConverter(TestCase):
"type": "object",
"properties": {"hostname": {"type": "string", "format": "hostname"}},
}
model = SchemaConverter.build(schema)
self.assertEqual(model(hostname="example.com").hostname, "example.com")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(hostname="invalid..hostname")
def test_string_format_datetime(self):
@@ -527,12 +553,14 @@ class TestSchemaConverter(TestCase):
"type": "object",
"properties": {"timestamp": {"type": "string", "format": "date-time"}},
}
model = SchemaConverter.build(schema)
self.assertEqual(
model(timestamp="2024-01-01T12:00:00Z").timestamp.isoformat(),
"2024-01-01T12:00:00+00:00",
)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(timestamp="invalid-datetime")
def test_string_format_time(self):
@@ -541,11 +569,13 @@ class TestSchemaConverter(TestCase):
"type": "object",
"properties": {"time": {"type": "string", "format": "time"}},
}
model = SchemaConverter.build(schema)
self.assertEqual(
model(time="20:20:39+00:00").time.isoformat(), "20:20:39+00:00"
)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
model(time="25:00:00")
def test_string_format_unsupported(self):
@@ -554,7 +584,8 @@ class TestSchemaConverter(TestCase):
"type": "object",
"properties": {"field": {"type": "string", "format": "unsupported"}},
}
with self.assertRaises(ValueError):
with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(schema)
def test_ref_with_root_ref(self):
@@ -712,10 +743,10 @@ class TestSchemaConverter(TestCase):
obj = Model()
self.assertEqual(obj.name, "United States of America")
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
obj.name = "Canada"
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(name="Canada")
def test_const_type_parser_with_non_hashable_value(self):
@@ -735,10 +766,10 @@ class TestSchemaConverter(TestCase):
obj = Model()
self.assertEqual(obj.name, ["Brazil"])
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
obj.name = ["Argentina"]
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(name=["Argentina"])
def test_null_type_parser(self):
@@ -758,5 +789,5 @@ class TestSchemaConverter(TestCase):
obj = Model(a_thing=None)
self.assertIsNone(obj.a_thing)
with self.assertRaises(ValueError):
with self.assertRaises(ValidationError):
Model(a_thing="not none")

149
uv.lock generated
View File

@@ -326,13 +326,18 @@ dependencies = [
[package.dev-dependencies]
dev = [
{ name = "coverage" },
{ name = "mypy" },
{ name = "poethepoet" },
{ name = "pre-commit" },
{ name = "ruff" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-autobuild" },
{ name = "sphinx-autobuild", version = "2024.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx-autobuild", version = "2025.8.25", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx-autodoc-typehints", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-rtd-theme" },
{ name = "types-jsonschema" },
]
[package.metadata]
@@ -345,12 +350,15 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
{ name = "coverage", specifier = ">=7.8.0" },
{ name = "mypy", specifier = ">=1.18.1" },
{ name = "poethepoet", specifier = ">=0.33.1" },
{ name = "pre-commit", specifier = ">=4.2.0" },
{ name = "ruff", specifier = ">=0.11.4" },
{ name = "sphinx", specifier = ">=8.1.3" },
{ name = "sphinx-autobuild", specifier = ">=2024.10.3" },
{ name = "sphinx-autodoc-typehints", specifier = ">=3.0.1" },
{ name = "sphinx-rtd-theme", specifier = ">=3.0.2" },
{ name = "types-jsonschema", specifier = ">=4.25.1.20250822" },
]
[[package]]
@@ -450,6 +458,60 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
]
[[package]]
name = "mypy"
version = "1.18.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/14/a3/931e09fc02d7ba96da65266884da4e4a8806adcdb8a57faaacc6edf1d538/mypy-1.18.1.tar.gz", hash = "sha256:9e988c64ad3ac5987f43f5154f884747faf62141b7f842e87465b45299eea5a9", size = 3448447, upload-time = "2025-09-11T23:00:47.067Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/06/29ea5a34c23938ae93bc0040eb2900eb3f0f2ef4448cc59af37ab3ddae73/mypy-1.18.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2761b6ae22a2b7d8e8607fb9b81ae90bc2e95ec033fd18fa35e807af6c657763", size = 12811535, upload-time = "2025-09-11T22:58:55.399Z" },
{ url = "https://files.pythonhosted.org/packages/a8/40/04c38cb04fa9f1dc224b3e9634021a92c47b1569f1c87dfe6e63168883bb/mypy-1.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b10e3ea7f2eec23b4929a3fabf84505da21034a4f4b9613cda81217e92b74f3", size = 11897559, upload-time = "2025-09-11T22:59:48.041Z" },
{ url = "https://files.pythonhosted.org/packages/46/bf/4c535bd45ea86cebbc1a3b6a781d442f53a4883f322ebd2d442db6444d0b/mypy-1.18.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:261fbfced030228bc0f724d5d92f9ae69f46373bdfd0e04a533852677a11dbea", size = 12507430, upload-time = "2025-09-11T22:59:30.415Z" },
{ url = "https://files.pythonhosted.org/packages/e2/e1/cbefb16f2be078d09e28e0b9844e981afb41f6ffc85beb68b86c6976e641/mypy-1.18.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dc6b34a1c6875e6286e27d836a35c0d04e8316beac4482d42cfea7ed2527df8", size = 13243717, upload-time = "2025-09-11T22:59:11.297Z" },
{ url = "https://files.pythonhosted.org/packages/65/e8/3e963da63176f16ca9caea7fa48f1bc8766de317cd961528c0391565fd47/mypy-1.18.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1cabb353194d2942522546501c0ff75c4043bf3b63069cb43274491b44b773c9", size = 13492052, upload-time = "2025-09-11T23:00:09.29Z" },
{ url = "https://files.pythonhosted.org/packages/4b/09/d5d70c252a3b5b7530662d145437bd1de15f39fa0b48a27ee4e57d254aa1/mypy-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:738b171690c8e47c93569635ee8ec633d2cdb06062f510b853b5f233020569a9", size = 9765846, upload-time = "2025-09-11T22:58:26.198Z" },
{ url = "https://files.pythonhosted.org/packages/32/28/47709d5d9e7068b26c0d5189c8137c8783e81065ad1102b505214a08b548/mypy-1.18.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c903857b3e28fc5489e54042684a9509039ea0aedb2a619469438b544ae1961", size = 12734635, upload-time = "2025-09-11T23:00:24.983Z" },
{ url = "https://files.pythonhosted.org/packages/7c/12/ee5c243e52497d0e59316854041cf3b3130131b92266d0764aca4dec3c00/mypy-1.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a0c8392c19934c2b6c65566d3a6abdc6b51d5da7f5d04e43f0eb627d6eeee65", size = 11817287, upload-time = "2025-09-11T22:59:07.38Z" },
{ url = "https://files.pythonhosted.org/packages/48/bd/2aeb950151005fe708ab59725afed7c4aeeb96daf844f86a05d4b8ac34f8/mypy-1.18.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f85eb7efa2ec73ef63fc23b8af89c2fe5bf2a4ad985ed2d3ff28c1bb3c317c92", size = 12430464, upload-time = "2025-09-11T22:58:48.084Z" },
{ url = "https://files.pythonhosted.org/packages/71/e8/7a20407aafb488acb5734ad7fb5e8c2ef78d292ca2674335350fa8ebef67/mypy-1.18.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82ace21edf7ba8af31c3308a61dc72df30500f4dbb26f99ac36b4b80809d7e94", size = 13164555, upload-time = "2025-09-11T23:00:13.803Z" },
{ url = "https://files.pythonhosted.org/packages/e8/c9/5f39065252e033b60f397096f538fb57c1d9fd70a7a490f314df20dd9d64/mypy-1.18.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a2dfd53dfe632f1ef5d161150a4b1f2d0786746ae02950eb3ac108964ee2975a", size = 13359222, upload-time = "2025-09-11T23:00:33.469Z" },
{ url = "https://files.pythonhosted.org/packages/85/b6/d54111ef3c1e55992cd2ec9b8b6ce9c72a407423e93132cae209f7e7ba60/mypy-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:320f0ad4205eefcb0e1a72428dde0ad10be73da9f92e793c36228e8ebf7298c0", size = 9760441, upload-time = "2025-09-11T23:00:44.826Z" },
{ url = "https://files.pythonhosted.org/packages/e7/14/1c3f54d606cb88a55d1567153ef3a8bc7b74702f2ff5eb64d0994f9e49cb/mypy-1.18.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:502cde8896be8e638588b90fdcb4c5d5b8c1b004dfc63fd5604a973547367bb9", size = 12911082, upload-time = "2025-09-11T23:00:41.465Z" },
{ url = "https://files.pythonhosted.org/packages/90/83/235606c8b6d50a8eba99773add907ce1d41c068edb523f81eb0d01603a83/mypy-1.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7509549b5e41be279afc1228242d0e397f1af2919a8f2877ad542b199dc4083e", size = 11919107, upload-time = "2025-09-11T22:58:40.903Z" },
{ url = "https://files.pythonhosted.org/packages/ca/25/4e2ce00f8d15b99d0c68a2536ad63e9eac033f723439ef80290ec32c1ff5/mypy-1.18.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5956ecaabb3a245e3f34100172abca1507be687377fe20e24d6a7557e07080e2", size = 12472551, upload-time = "2025-09-11T22:58:37.272Z" },
{ url = "https://files.pythonhosted.org/packages/32/bb/92642a9350fc339dd9dcefcf6862d171b52294af107d521dce075f32f298/mypy-1.18.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8750ceb014a96c9890421c83f0db53b0f3b8633e2864c6f9bc0a8e93951ed18d", size = 13340554, upload-time = "2025-09-11T22:59:38.756Z" },
{ url = "https://files.pythonhosted.org/packages/cd/ee/38d01db91c198fb6350025d28f9719ecf3c8f2c55a0094bfbf3ef478cc9a/mypy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fb89ea08ff41adf59476b235293679a6eb53a7b9400f6256272fb6029bec3ce5", size = 13530933, upload-time = "2025-09-11T22:59:20.228Z" },
{ url = "https://files.pythonhosted.org/packages/da/8d/6d991ae631f80d58edbf9d7066e3f2a96e479dca955d9a968cd6e90850a3/mypy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:2657654d82fcd2a87e02a33e0d23001789a554059bbf34702d623dafe353eabf", size = 9828426, upload-time = "2025-09-11T23:00:21.007Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ec/ef4a7260e1460a3071628a9277a7579e7da1b071bc134ebe909323f2fbc7/mypy-1.18.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d70d2b5baf9b9a20bc9c730015615ae3243ef47fb4a58ad7b31c3e0a59b5ef1f", size = 12918671, upload-time = "2025-09-11T22:58:29.814Z" },
{ url = "https://files.pythonhosted.org/packages/a1/82/0ea6c3953f16223f0b8eda40c1aeac6bd266d15f4902556ae6e91f6fca4c/mypy-1.18.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8367e33506300f07a43012fc546402f283c3f8bcff1dc338636affb710154ce", size = 11913023, upload-time = "2025-09-11T23:00:29.049Z" },
{ url = "https://files.pythonhosted.org/packages/ae/ef/5e2057e692c2690fc27b3ed0a4dbde4388330c32e2576a23f0302bc8358d/mypy-1.18.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:913f668ec50c3337b89df22f973c1c8f0b29ee9e290a8b7fe01cc1ef7446d42e", size = 12473355, upload-time = "2025-09-11T23:00:04.544Z" },
{ url = "https://files.pythonhosted.org/packages/98/43/b7e429fc4be10e390a167b0cd1810d41cb4e4add4ae50bab96faff695a3b/mypy-1.18.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a0e70b87eb27b33209fa4792b051c6947976f6ab829daa83819df5f58330c71", size = 13346944, upload-time = "2025-09-11T22:58:23.024Z" },
{ url = "https://files.pythonhosted.org/packages/89/4e/899dba0bfe36bbd5b7c52e597de4cf47b5053d337b6d201a30e3798e77a6/mypy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c378d946e8a60be6b6ede48c878d145546fb42aad61df998c056ec151bf6c746", size = 13512574, upload-time = "2025-09-11T22:59:52.152Z" },
{ url = "https://files.pythonhosted.org/packages/f5/f8/7661021a5b0e501b76440454d786b0f01bb05d5c4b125fcbda02023d0250/mypy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:2cd2c1e0f3a7465f22731987fff6fc427e3dcbb4ca5f7db5bbeaff2ff9a31f6d", size = 9837684, upload-time = "2025-09-11T22:58:44.454Z" },
{ url = "https://files.pythonhosted.org/packages/bf/87/7b173981466219eccc64c107cf8e5ab9eb39cc304b4c07df8e7881533e4f/mypy-1.18.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ba24603c58e34dd5b096dfad792d87b304fc6470cbb1c22fd64e7ebd17edcc61", size = 12900265, upload-time = "2025-09-11T22:59:03.4Z" },
{ url = "https://files.pythonhosted.org/packages/ae/cc/b10e65bae75b18a5ac8f81b1e8e5867677e418f0dd2c83b8e2de9ba96ebd/mypy-1.18.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed36662fb92ae4cb3cacc682ec6656208f323bbc23d4b08d091eecfc0863d4b5", size = 11942890, upload-time = "2025-09-11T23:00:00.607Z" },
{ url = "https://files.pythonhosted.org/packages/39/d4/aeefa07c44d09f4c2102e525e2031bc066d12e5351f66b8a83719671004d/mypy-1.18.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:040ecc95e026f71a9ad7956fea2724466602b561e6a25c2e5584160d3833aaa8", size = 12472291, upload-time = "2025-09-11T22:59:43.425Z" },
{ url = "https://files.pythonhosted.org/packages/c6/07/711e78668ff8e365f8c19735594ea95938bff3639a4c46a905e3ed8ff2d6/mypy-1.18.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:937e3ed86cb731276706e46e03512547e43c391a13f363e08d0fee49a7c38a0d", size = 13318610, upload-time = "2025-09-11T23:00:17.604Z" },
{ url = "https://files.pythonhosted.org/packages/ca/85/df3b2d39339c31d360ce299b418c55e8194ef3205284739b64962f6074e7/mypy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f95cc4f01c0f1701ca3b0355792bccec13ecb2ec1c469e5b85a6ef398398b1d", size = 13513697, upload-time = "2025-09-11T22:58:59.534Z" },
{ url = "https://files.pythonhosted.org/packages/b1/df/462866163c99ea73bb28f0eb4d415c087e30de5d36ee0f5429d42e28689b/mypy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:e4f16c0019d48941220ac60b893615be2f63afedaba6a0801bdcd041b96991ce", size = 9985739, upload-time = "2025-09-11T22:58:51.644Z" },
{ url = "https://files.pythonhosted.org/packages/e0/1d/4b97d3089b48ef3d904c9ca69fab044475bd03245d878f5f0b3ea1daf7ce/mypy-1.18.1-py3-none-any.whl", hash = "sha256:b76a4de66a0ac01da1be14ecc8ae88ddea33b8380284a9e3eae39d57ebcbe26e", size = 2352212, upload-time = "2025-09-11T22:59:26.576Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]]
name = "nodeenv"
version = "1.9.1"
@@ -477,6 +539,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "platformdirs"
version = "4.3.7"
@@ -901,20 +972,72 @@ wheels = [
name = "sphinx-autobuild"
version = "2024.10.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
{ name = "colorama" },
{ name = "colorama", marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "starlette" },
{ name = "uvicorn" },
{ name = "watchfiles" },
{ name = "websockets" },
{ name = "starlette", marker = "python_full_version < '3.11'" },
{ name = "uvicorn", marker = "python_full_version < '3.11'" },
{ name = "watchfiles", marker = "python_full_version < '3.11'" },
{ name = "websockets", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908, upload-time = "2024-10-02T23:15:28.739Z" },
]
[[package]]
name = "sphinx-autobuild"
version = "2025.8.25"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.11'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.11'" },
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "starlette", marker = "python_full_version >= '3.11'" },
{ name = "uvicorn", marker = "python_full_version >= '3.11'" },
{ name = "watchfiles", marker = "python_full_version >= '3.11'" },
{ name = "websockets", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" },
]
[[package]]
name = "sphinx-autodoc-typehints"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.11'",
]
dependencies = [
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/26/f0/43c6a5ff3e7b08a8c3b32f81b859f1b518ccc31e45f22e2b41ced38be7b9/sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55", size = 36282, upload-time = "2025-01-16T18:25:30.958Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/dc/dc46c5c7c566b7ec5e8f860f9c89533bf03c0e6aadc96fb9b337867e4460/sphinx_autodoc_typehints-3.0.1-py3-none-any.whl", hash = "sha256:4b64b676a14b5b79cefb6628a6dc8070e320d4963e8ff640a2f3e9390ae9045a", size = 20245, upload-time = "2025-01-16T18:25:27.394Z" },
]
[[package]]
name = "sphinx-autodoc-typehints"
version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.11'",
]
dependencies = [
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/93/68/a388a9b8f066cd865d9daa65af589d097efbfab9a8c302d2cb2daa43b52e/sphinx_autodoc_typehints-3.2.0.tar.gz", hash = "sha256:107ac98bc8b4837202c88c0736d59d6da44076e65a0d7d7d543a78631f662a9b", size = 36724, upload-time = "2025-04-25T16:53:25.872Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl", hash = "sha256:884b39be23b1d884dcc825d4680c9c6357a476936e3b381a67ae80091984eb49", size = 20563, upload-time = "2025-04-25T16:53:24.492Z" },
]
[[package]]
name = "sphinx-rtd-theme"
version = "3.0.2"
@@ -1048,6 +1171,18 @@ wheels = [
{ 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 = "types-jsonschema"
version = "4.25.1.20250822"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "referencing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/7f/369b54dad6eb6b5adc1fb1c53edbed18e6c32cbc600357135308902fdbdc/types_jsonschema-4.25.1.20250822.tar.gz", hash = "sha256:aac69ed4b23f49aaceb7fcb834141d61b9e4e6a7f6008cb2f0d3b831dfa8464a", size = 15628, upload-time = "2025-08-22T03:04:18.293Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/3d/bc1d171f032fcf63cedd4ade241f3f4e66d7e3bb53ee1da3c8f2f043eb0b/types_jsonschema-4.25.1.20250822-py3-none-any.whl", hash = "sha256:f82c2d7fa1ce1c0b84ba1de4ed6798469768188884db04e66421913a4e181294", size = 15923, upload-time = "2025-08-22T03:04:17.346Z" },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"