19 Commits

Author SHA1 Message Date
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
44 changed files with 798 additions and 312 deletions

View File

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

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: :show-inheritance:
:undoc-members: :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 jambo.parser.float\_type\_parser module
--------------------------------------- ---------------------------------------
@@ -52,6 +68,14 @@ jambo.parser.int\_type\_parser module
:show-inheritance: :show-inheritance:
:undoc-members: :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 jambo.parser.object\_type\_parser module
---------------------------------------- ----------------------------------------
@@ -60,6 +84,14 @@ jambo.parser.object\_type\_parser module
:show-inheritance: :show-inheritance:
:undoc-members: :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 jambo.parser.ref\_type\_parser module
------------------------------------- -------------------------------------

View File

@@ -7,6 +7,7 @@ Subpackages
.. toctree:: .. toctree::
:maxdepth: 4 :maxdepth: 4
jambo.exceptions
jambo.parser jambo.parser
jambo.types 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

@@ -29,4 +29,4 @@ __all__ = [
"OneOfTypeParser", "OneOfTypeParser",
"StringTypeParser", "StringTypeParser",
"RefTypeParser", "RefTypeParser",
] ]

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

View File

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

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser._type_parser import GenericTypeParser from jambo.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions from jambo.types.type_parser_options import TypeParserOptions
@@ -14,10 +15,15 @@ class AnyOfTypeParser(GenericTypeParser):
self, name, properties, **kwargs: Unpack[TypeParserOptions] self, name, properties, **kwargs: Unpack[TypeParserOptions]
): ):
if "anyOf" not in properties: 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): 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) 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.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions from jambo.types.type_parser_options import TypeParserOptions
@@ -26,8 +27,15 @@ class ArrayTypeParser(GenericTypeParser):
): ):
item_properties = kwargs.copy() item_properties = kwargs.copy()
item_properties["required"] = True 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( _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 wrapper_type = set if properties.get("uniqueItems", False) else list
@@ -47,8 +55,9 @@ class ArrayTypeParser(GenericTypeParser):
return lambda: None return lambda: None
if not isinstance(default_list, Iterable): if not isinstance(default_list, Iterable):
raise ValueError( raise InvalidSchemaException(
f"Default value for array must be an iterable, got {type(default_list)}" f"Default value for array must be an iterable, got {type(default_list)}",
invalid_field="default",
) )
return lambda: copy.deepcopy(wrapper_type(default_list)) 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.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions from jambo.types.type_parser_options import TypeParserOptions
@@ -20,6 +21,9 @@ class BooleanTypeParser(GenericTypeParser):
default_value = properties.get("default") default_value = properties.get("default")
if default_value is not None and not isinstance(default_value, bool): 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 return bool, mapped_properties

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
from jambo.parser._type_parser import GenericTypeParser from jambo.parser._type_parser import GenericTypeParser
from jambo.types.json_schema_type import JSONSchema
from jambo.types.type_parser_options import TypeParserOptions from jambo.types.type_parser_options import TypeParserOptions
from pydantic import BaseModel, ConfigDict, Field, create_model 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): class ObjectTypeParser(GenericTypeParser):
@@ -11,7 +13,7 @@ class ObjectTypeParser(GenericTypeParser):
json_schema_type = "type:object" json_schema_type = "type:object"
def from_properties_impl( 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]: ) -> tuple[type[BaseModel], dict]:
type_parsing = self.to_model( type_parsing = self.to_model(
name, name,
@@ -32,29 +34,29 @@ class ObjectTypeParser(GenericTypeParser):
def to_model( def to_model(
cls, cls,
name: str, name: str,
schema: dict[str, Any], properties: dict[str, JSONSchema],
required_keys: list[str], required_keys: list[str],
**kwargs: Unpack[TypeParserOptions], **kwargs: Unpack[TypeParserOptions],
) -> type[BaseModel]: ) -> type[BaseModel]:
""" """
Converts JSON Schema object properties to a Pydantic model. Converts JSON Schema object properties to a Pydantic model.
:param name: The name of the 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. :param required_keys: List of required keys in the schema.
:return: A Pydantic model class. :return: A Pydantic model class.
""" """
model_config = ConfigDict(validate_assignment=True) 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 @classmethod
def _parse_properties( def _parse_properties(
cls, cls,
properties: dict[str, Any], properties: dict[str, JSONSchema],
required_keys: list[str], required_keys: list[str],
**kwargs: Unpack[TypeParserOptions], **kwargs: Unpack[TypeParserOptions],
) -> dict[str, tuple[type, Field]]: ) -> dict[str, tuple[type, FieldInfo]]:
required_keys = required_keys or [] required_keys = required_keys or []
fields = {} fields = {}
@@ -63,7 +65,9 @@ class ObjectTypeParser(GenericTypeParser):
sub_property["required"] = name in required_keys sub_property["required"] = name in required_keys
parsed_type, parsed_properties = GenericTypeParser.type_from_properties( 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)) 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.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions 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 from typing_extensions import Annotated, Any, Union, Unpack, get_args
Annotation = Annotated[Any, ...]
class OneOfTypeParser(GenericTypeParser): class OneOfTypeParser(GenericTypeParser):
mapped_type = Union mapped_type = Union
@@ -14,10 +18,14 @@ class OneOfTypeParser(GenericTypeParser):
self, name, properties, **kwargs: Unpack[TypeParserOptions] self, name, properties, **kwargs: Unpack[TypeParserOptions]
): ):
if "oneOf" not in properties: 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: 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) mapped_properties = self.mappings_properties_builder(properties, **kwargs)
@@ -49,13 +57,15 @@ class OneOfTypeParser(GenericTypeParser):
@staticmethod @staticmethod
def _build_type_one_of_with_discriminator( def _build_type_one_of_with_discriminator(
subfield_types: list[Annotated], discriminator_prop: dict subfield_types: list[Annotation], discriminator_prop: dict
) -> Annotated: ) -> Annotation:
""" """
Build a type with a discriminator. Build a type with a discriminator.
""" """
if not isinstance(discriminator_prop, dict): 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: for field in subfield_types:
field_type, field_info = get_args(field) field_type, field_info = get_args(field)
@@ -63,18 +73,22 @@ class OneOfTypeParser(GenericTypeParser):
if issubclass(field_type, BaseModel): if issubclass(field_type, BaseModel):
continue continue
raise ValueError( raise InvalidSchemaException(
"When using a discriminator, all subfield types must be of type 'object'." "When using a discriminator, all subfield types must be of type 'object'.",
invalid_field="discriminator",
) )
property_name = discriminator_prop.get("propertyName") property_name = discriminator_prop.get("propertyName")
if property_name is None or not isinstance(property_name, str): 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)] return Annotated[Union[(*subfield_types,)], Field(discriminator=property_name)]
@staticmethod @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. 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.parser import GenericTypeParser
from jambo.types.json_schema_type import JSONSchema
from jambo.types.type_parser_options import TypeParserOptions 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"] RefStrategy = Literal["forward_ref", "def_ref"]
@@ -13,21 +15,22 @@ class RefTypeParser(GenericTypeParser):
json_schema_type = "$ref" json_schema_type = "$ref"
def from_properties_impl( 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]: ) -> tuple[RefType, dict]:
if "$ref" not in properties: 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 kwargs.get("context") is None:
if context is None: raise InternalAssertionException(
raise RuntimeError( "`context` must be provided in kwargs for RefTypeParser"
f"RefTypeParser: Missing `content` in properties for {name}"
) )
ref_cache = kwargs.get("ref_cache") ref_cache = kwargs.get("ref_cache")
if ref_cache is None: if ref_cache is None:
raise RuntimeError( raise InternalAssertionException(
f"RefTypeParser: Missing `ref_cache` in properties for {name}" "`ref_cache` must be provided in kwargs for RefTypeParser"
) )
mapped_properties = self.mappings_properties_builder(properties, **kwargs) mapped_properties = self.mappings_properties_builder(properties, **kwargs)
@@ -41,19 +44,19 @@ class RefTypeParser(GenericTypeParser):
# If the reference is either processing or already cached # If the reference is either processing or already cached
return ref_state, mapped_properties return ref_state, mapped_properties
ref_cache[ref_name] = self._parse_from_strategy( ref = self._parse_from_strategy(ref_strategy, ref_name, ref_property, **kwargs)
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( def _parse_from_strategy(
self, self,
ref_strategy: RefStrategy, ref_strategy: RefStrategy,
ref_name: str, ref_name: str,
ref_property: dict[str, Any], ref_property: JSONSchema,
**kwargs: Unpack[TypeParserOptions], **kwargs: Unpack[TypeParserOptions],
): ) -> RefType:
mapped_type: RefType
match ref_strategy: match ref_strategy:
case "forward_ref": case "forward_ref":
mapped_type = ForwardRef(ref_name) mapped_type = ForwardRef(ref_name)
@@ -62,14 +65,14 @@ class RefTypeParser(GenericTypeParser):
ref_name, ref_property, **kwargs ref_name, ref_property, **kwargs
) )
case _: case _:
raise ValueError( raise InvalidSchemaException(
f"RefTypeParser: Unsupported $ref {ref_property['$ref']}" f"Unsupported $ref {ref_property['$ref']}", invalid_field="$ref"
) )
return mapped_type return mapped_type
def _get_ref_from_cache( 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: ) -> RefType | type | None:
try: try:
ref_state = ref_cache[ref_name] 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 # If the reference is not in the cache, we will set it to None
ref_cache[ref_name] = None ref_cache[ref_name] = None
return None
def _examine_ref_strategy( def _examine_ref_strategy(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions] self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
) -> tuple[RefStrategy, str, dict] | None: ) -> tuple[RefStrategy, str, JSONSchema]:
if properties["$ref"] == "#": if properties.get("$ref") == "#":
ref_name = kwargs["context"].get("title") ref_name = kwargs["context"].get("title")
if ref_name is None: if ref_name is None:
raise ValueError( raise InvalidSchemaException(
"RefTypeParser: Missing title in properties for $ref of Root Reference" "Missing title in properties for $ref of Root Reference",
invalid_field="title",
) )
return "forward_ref", ref_name, {} return "forward_ref", ref_name, {}
if properties["$ref"].startswith("#/$defs/"): if properties.get("$ref", "").startswith("#/$defs/"):
target_name, target_property = self._extract_target_ref( target_name, target_property = self._extract_target_ref(
name, properties, **kwargs name, properties, **kwargs
) )
return "def_ref", target_name, target_property return "def_ref", target_name, target_property
raise ValueError( raise InvalidSchemaException(
"RefTypeParser: Only Root and $defs references are supported at the moment" "Only Root and $defs references are supported at the moment",
invalid_field="$ref",
) )
def _extract_target_ref( def _extract_target_ref(
self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions] self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions]
) -> tuple[str, dict]: ) -> tuple[str, JSONSchema]:
target_name = None target_name = None
target_property = kwargs["context"] target_property = kwargs["context"]
for prop_name in properties["$ref"].split("/")[1:]: for prop_name in properties["$ref"].split("/")[1:]:
if prop_name not in target_property: if prop_name not in target_property:
raise ValueError( raise InvalidSchemaException(
f"RefTypeParser: Missing {prop_name} in" f"Missing {prop_name} in properties for $ref {properties['$ref']}",
" properties for $ref {properties['$ref']}" invalid_field=prop_name,
) )
target_name = 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: if not isinstance(target_name, str) or target_property is None:
raise ValueError(f"RefTypeParser: Invalid $ref {properties['$ref']}") raise InvalidSchemaException(
f"Invalid $ref {properties['$ref']}", invalid_field="$ref"
)
return target_name, target_property 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.parser._type_parser import GenericTypeParser
from jambo.types.type_parser_options import TypeParserOptions from jambo.types.type_parser_options import TypeParserOptions
@@ -22,19 +23,19 @@ class StringTypeParser(GenericTypeParser):
} }
format_type_mapping = { 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, "date": date,
"time": time, "time": time,
"date-time": datetime, "date-time": datetime,
"duration": timedelta, "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, "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, "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, "ipv4": IPv4Address,
"ipv6": IPv6Address, "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, "uri": AnyUrl,
# "iri" # Not supported by pydantic and currently not supported by jambo # "iri" # Not supported by pydantic and currently not supported by jambo
"uuid": UUID, "uuid": UUID,
@@ -54,7 +55,9 @@ class StringTypeParser(GenericTypeParser):
return str, mapped_properties return str, mapped_properties
if format_type not in self.format_type_mapping: 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] mapped_type = self.format_type_mapping[format_type]
if format_type in self.format_pattern_mapping: if format_type in self.format_pattern_mapping:

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.parser import ObjectTypeParser, RefTypeParser
from jambo.types.json_schema_type import JSONSchema from jambo.types import JSONSchema
from jsonschema.exceptions import SchemaError from jsonschema.exceptions import SchemaError
from jsonschema.validators import validator_for from jsonschema.validators import validator_for
@@ -25,12 +26,16 @@ class SchemaConverter:
try: try:
validator = validator_for(schema) validator = validator_for(schema)
validator.check_schema(schema) validator.check_schema(schema) # type: ignore
except SchemaError as e: except SchemaError as err:
raise ValueError(f"Invalid JSON Schema: {e}") raise InvalidSchemaException(
"Validation of JSON Schema failed.", cause=err
) from err
if "title" not in schema: 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) schema_type = SchemaConverter._get_schema_type(schema)
@@ -38,10 +43,11 @@ class SchemaConverter:
case "object": case "object":
return ObjectTypeParser.to_model( return ObjectTypeParser.to_model(
schema["title"], schema["title"],
schema["properties"], schema.get("properties", {}),
schema.get("required", []), schema.get("required", []),
context=schema, context=schema,
ref_cache=dict(), ref_cache=dict(),
required=True,
) )
case "$ref": case "$ref":
@@ -50,13 +56,20 @@ class SchemaConverter:
schema, schema,
context=schema, context=schema,
ref_cache=dict(), ref_cache=dict(),
required=True,
) )
return parsed_model return parsed_model
case _: 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 @staticmethod
def _get_schema_type(schema: JSONSchema) -> str: def _get_schema_type(schema: JSONSchema) -> str | None:
""" """
Returns the type of the schema. Returns the type of the schema.
:param schema: The JSON Schema to check. :param schema: The JSON Schema to check.
@@ -65,4 +78,4 @@ class SchemaConverter:
if "$ref" in schema: if "$ref" in schema:
return "$ref" 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 from types import NoneType
# Primitive JSON types
JSONSchemaType = Literal[ JSONSchemaType = Literal[
"string", "number", "integer", "boolean", "object", "array", "null" "string", "number", "integer", "boolean", "object", "array", "null"
] ]
JSONSchemaNativeTypes: tuple[type, ...] = ( JSONSchemaNativeTypes: tuple[type, ...] = (
str, str,
int,
float, float,
int,
bool, bool,
list, list,
set, set,
NoneType, NoneType,
) )
JSONType = Union[str, int, float, bool, None, Dict[str, "JSONType"], List["JSONType"]] JSONType = Union[str, int, float, bool, None, Dict[str, "JSONType"], List["JSONType"]]
# Dynamically define TypedDict with JSON Schema keywords
class JSONSchema(TypedDict, total=False): JSONSchema = TypedDict(
# Basic metadata "JSONSchema",
title: str {
description: str "$id": str,
default: JSONType "$schema": str,
examples: List[JSONType] "$ref": str,
"$anchor": str,
# Type definitions "$comment": str,
type: Union[JSONSchemaType, List[JSONSchemaType]] "$defs": Dict[str, "JSONSchema"],
"title": str,
# Object-specific keywords "description": str,
properties: Dict[str, "JSONSchema"] "default": JSONType,
required: List[str] "examples": List[JSONType],
additionalProperties: Union[bool, "JSONSchema"] "type": JSONSchemaType,
minProperties: int "enum": List[JSONType],
maxProperties: int "const": JSONType,
patternProperties: Dict[str, "JSONSchema"] "properties": Dict[str, "JSONSchema"],
dependencies: Dict[str, Union[List[str], "JSONSchema"]] "patternProperties": Dict[str, "JSONSchema"],
"additionalProperties": Union[bool, "JSONSchema"],
# Array-specific keywords "required": List[str],
items: Union["JSONSchema", List["JSONSchema"]] "minProperties": int,
additionalItems: Union[bool, "JSONSchema"] "maxProperties": int,
minItems: int "dependencies": Dict[str, Union[List[str], "JSONSchema"]],
maxItems: int "items": "JSONSchema",
uniqueItems: bool "prefixItems": List["JSONSchema"],
"additionalItems": Union[bool, "JSONSchema"],
# String-specific keywords "contains": "JSONSchema",
minLength: int "minItems": int,
maxLength: int "maxItems": int,
pattern: str "uniqueItems": bool,
format: str "minLength": int,
"maxLength": int,
# Number-specific keywords "pattern": str,
minimum: float "format": str,
maximum: float "minimum": float,
exclusiveMinimum: float "maximum": float,
exclusiveMaximum: float "exclusiveMinimum": Union[bool, float],
multipleOf: float "exclusiveMaximum": Union[bool, float],
"multipleOf": float,
# Enum and const "if": "JSONSchema",
enum: List[JSONType] "then": "JSONSchema",
const: JSONType "else": "JSONSchema",
"allOf": List["JSONSchema"],
# Conditionals "anyOf": List["JSONSchema"],
if_: "JSONSchema" # 'if' is a reserved word in Python "oneOf": List["JSONSchema"],
then: "JSONSchema" "not": "JSONSchema",
else_: "JSONSchema" # 'else' is also a reserved word },
total=False, # all fields optional
# 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

View File

@@ -1,9 +1,9 @@
from jambo.types.json_schema_type import JSONSchema from jambo.types.json_schema_type import JSONSchema
from typing_extensions import TypedDict from typing_extensions import ForwardRef, TypedDict
class TypeParserOptions(TypedDict): class TypeParserOptions(TypedDict):
required: bool required: bool
context: JSONSchema 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.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
] ]
license = { file = "LICENSE" } license = "MIT"
readme = "README.md" readme = "README.md"
# Project Dependencies # Project Dependencies
@@ -31,12 +31,14 @@ dependencies = [
[dependency-groups] [dependency-groups]
dev = [ dev = [
"coverage>=7.8.0", "coverage>=7.8.0",
"mypy>=1.18.1",
"poethepoet>=0.33.1", "poethepoet>=0.33.1",
"pre-commit>=4.2.0", "pre-commit>=4.2.0",
"ruff>=0.11.4", "ruff>=0.11.4",
"sphinx>=8.1.3", "sphinx>=8.1.3",
"sphinx-autobuild>=2024.10.3", "sphinx-autobuild>=2024.10.3",
"sphinx-rtd-theme>=3.0.2", "sphinx-rtd-theme>=3.0.2",
"types-jsonschema>=4.25.1.20250822",
] ]
@@ -50,7 +52,8 @@ repository = "https://github.com/HideyoshiNakazone/jambo.git"
create-hooks = "bash .githooks/set-hooks.sh" create-hooks = "bash .githooks/set-hooks.sh"
tests = "python -m coverage run -m unittest discover -v" tests = "python -m coverage run -m unittest discover -v"
tests-report = "python -m coverage xml" tests-report = "python -m coverage xml"
serve-docs = "sphinx-autobuild docs/source docs/build" type-check = "mypy jambo"
serve-docs = "sphinx-autobuild docs/source docs/build"
# Build System # Build System
[tool.hatch.version] [tool.hatch.version]

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 jambo.parser.allof_type_parser import AllOfTypeParser
from pydantic import ValidationError
from unittest import TestCase from unittest import TestCase
@@ -42,13 +45,13 @@ class TestAllOfTypeParser(TestCase):
"placeholder", properties "placeholder", properties
) )
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
type_parsing(name="John", age=101) type_parsing(name="John", age=101)
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
type_parsing(name="", age=30) type_parsing(name="", age=30)
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
type_parsing(name="John Invalid", age=30) type_parsing(name="John Invalid", age=30)
obj = type_parsing(name="John", age=30) obj = type_parsing(name="John", age=30)
@@ -87,10 +90,10 @@ class TestAllOfTypeParser(TestCase):
"placeholder", properties "placeholder", properties
) )
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
type_parsing(name="John") type_parsing(name="John")
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
type_parsing(age=30) type_parsing(age=30)
obj = type_parsing(name="John", 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) AllOfTypeParser().from_properties("placeholder", properties)
def test_all_of_invalid_type_not_present(self): 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) AllOfTypeParser().from_properties("placeholder", properties)
def test_all_of_invalid_type_in_fields(self): 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) AllOfTypeParser().from_properties("placeholder", properties)
def test_all_of_invalid_type_not_all_equal(self): 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) AllOfTypeParser().from_properties("placeholder", properties)
def test_all_of_description_field(self): 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) 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 jambo.parser.anyof_type_parser import AnyOfTypeParser
from typing_extensions import Annotated, Union, get_args, get_origin 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) AnyOfTypeParser().from_properties("placeholder", properties)
def test_any_of_with_invalid_properties(self): def test_any_of_with_invalid_properties(self):
@@ -22,7 +23,7 @@ class TestAnyOfTypeParser(TestCase):
"anyOf": None, "anyOf": None,
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
AnyOfTypeParser().from_properties("placeholder", properties) AnyOfTypeParser().from_properties("placeholder", properties)
def test_any_of_string_or_int(self): def test_any_of_string_or_int(self):
@@ -95,5 +96,5 @@ class TestAnyOfTypeParser(TestCase):
"default": 3.14, "default": 3.14,
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
AnyOfTypeParser().from_properties("placeholder", properties) AnyOfTypeParser().from_properties("placeholder", properties)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,15 @@
from jambo import SchemaConverter from jambo import SchemaConverter
from jambo.exceptions import InvalidSchemaException
from jambo.parser.oneof_type_parser import OneOfTypeParser from jambo.parser.oneof_type_parser import OneOfTypeParser
from pydantic import ValidationError
from unittest import TestCase from unittest import TestCase
class TestOneOfTypeParser(TestCase): class TestOneOfTypeParser(TestCase):
def test_oneof_raises_on_invalid_property(self): def test_oneof_raises_on_invalid_property(self):
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
OneOfTypeParser().from_properties_impl( OneOfTypeParser().from_properties_impl(
"test_field", "test_field",
{ {
@@ -17,7 +20,18 @@ class TestOneOfTypeParser(TestCase):
ref_cache={}, 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( SchemaConverter.build(
{ {
"title": "Test", "title": "Test",
@@ -71,13 +85,13 @@ class TestOneOfTypeParser(TestCase):
Model = SchemaConverter.build(schema) Model = SchemaConverter.build(schema)
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
Model(id=-5) Model(id=-5)
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
Model(id="invalid") Model(id="invalid")
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
Model(id=123.45) Model(id=123.45)
def test_oneof_with_conflicting_schemas(self): def test_oneof_with_conflicting_schemas(self):
@@ -103,11 +117,11 @@ class TestOneOfTypeParser(TestCase):
obj2 = Model(data=9) obj2 = Model(data=9)
self.assertEqual(obj2.data, 9) self.assertEqual(obj2.data, 9)
with self.assertRaises(ValueError) as cm: with self.assertRaises(ValidationError) as cm:
Model(data=6) Model(data=6)
self.assertIn("matches multiple oneOf schemas", str(cm.exception)) self.assertIn("matches multiple oneOf schemas", str(cm.exception))
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
Model(data=5) Model(data=5)
def test_oneof_with_objects(self): def test_oneof_with_objects(self):
@@ -147,7 +161,7 @@ class TestOneOfTypeParser(TestCase):
obj2 = Model(contact_info={"phone": "123-456-7890"}) obj2 = Model(contact_info={"phone": "123-456-7890"})
self.assertEqual(obj2.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"}) Model(contact_info={"email": "user@example.com", "phone": "123-456-7890"})
def test_oneof_with_discriminator_basic(self): def test_oneof_with_discriminator_basic(self):
@@ -190,14 +204,14 @@ class TestOneOfTypeParser(TestCase):
self.assertEqual(dog.pet.type, "dog") self.assertEqual(dog.pet.type, "dog")
self.assertEqual(dog.pet.barks, False) self.assertEqual(dog.pet.barks, False)
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
Model(pet={"type": "cat", "barks": True}) Model(pet={"type": "cat", "barks": True})
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
Model(pet={"type": "bird", "flies": True}) Model(pet={"type": "bird", "flies": True})
def test_oneof_with_invalid_types(self): def test_oneof_with_invalid_types(self):
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
SchemaConverter.build( SchemaConverter.build(
{ {
"title": "Pet", "title": "Pet",
@@ -301,13 +315,13 @@ class TestOneOfTypeParser(TestCase):
Model = SchemaConverter.build(schema) Model = SchemaConverter.build(schema)
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
Model(shape={"type": "triangle", "base": 5, "height": 3}) Model(shape={"type": "triangle", "base": 5, "height": 3})
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
Model(shape={"type": "circle", "side": 5}) Model(shape={"type": "circle", "side": 5})
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
Model(shape={"radius": 5}) Model(shape={"radius": 5})
def test_oneof_missing_properties(self): def test_oneof_missing_properties(self):
@@ -324,7 +338,7 @@ class TestOneOfTypeParser(TestCase):
}, },
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(schema) SchemaConverter.build(schema)
def test_oneof_invalid_properties(self): def test_oneof_invalid_properties(self):
@@ -336,7 +350,7 @@ class TestOneOfTypeParser(TestCase):
}, },
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
SchemaConverter.build(schema) SchemaConverter.build(schema)
def test_oneof_with_default_value(self): 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) SchemaConverter.build(schema)
def test_oneof_discriminator_without_property_name(self): def test_oneof_discriminator_without_property_name(self):
# Should throw because the spec determines propertyName is required for discriminator # Should throw because the spec determines propertyName is required for discriminator
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
SchemaConverter.build( SchemaConverter.build(
{ {
"title": "Test", "title": "Test",
@@ -409,7 +423,7 @@ class TestOneOfTypeParser(TestCase):
def test_oneof_discriminator_with_invalid_discriminator(self): def test_oneof_discriminator_with_invalid_discriminator(self):
# Should throw because a valid discriminator is required # Should throw because a valid discriminator is required
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
SchemaConverter.build( SchemaConverter.build(
{ {
"title": "Test", "title": "Test",
@@ -465,8 +479,9 @@ class TestOneOfTypeParser(TestCase):
self.assertEqual(obj2.value, "very long string") self.assertEqual(obj2.value, "very long string")
# Invalid: Medium string (matches BOTH schemas - violates oneOf) # 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 Model(value="hello") # 5 chars: matches maxLength=6 AND minLength=4
self.assertIn("matches multiple oneOf schemas", str(cm.exception)) self.assertIn("matches multiple oneOf schemas", str(cm.exception))
def test_oneof_shapes_discriminator_from_docs(self): def test_oneof_shapes_discriminator_from_docs(self):
@@ -515,5 +530,5 @@ class TestOneOfTypeParser(TestCase):
self.assertEqual(rectangle.shape.height, 20) self.assertEqual(rectangle.shape.height, 20)
# Invalid: Wrong properties for the type # Invalid: Wrong properties for the type
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
Model(shape={"type": "circle", "width": 10}) 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 jambo.parser import ObjectTypeParser, RefTypeParser
from pydantic import ValidationError
from typing import ForwardRef from typing import ForwardRef
from unittest import TestCase from unittest import TestCase
@@ -16,7 +19,7 @@ class TestRefTypeParser(TestCase):
"required": ["name", "age"], "required": ["name", "age"],
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
RefTypeParser().from_properties( RefTypeParser().from_properties(
"person", "person",
properties, properties,
@@ -40,7 +43,7 @@ class TestRefTypeParser(TestCase):
}, },
} }
with self.assertRaises(RuntimeError): with self.assertRaises(InternalAssertionException):
RefTypeParser().from_properties( RefTypeParser().from_properties(
"person", "person",
properties, properties,
@@ -63,7 +66,7 @@ class TestRefTypeParser(TestCase):
}, },
} }
with self.assertRaises(RuntimeError): with self.assertRaises(InternalAssertionException):
RefTypeParser().from_properties( RefTypeParser().from_properties(
"person", "person",
properties, properties,
@@ -77,7 +80,7 @@ class TestRefTypeParser(TestCase):
"$ref": "https://example.com/schemas/person.json", "$ref": "https://example.com/schemas/person.json",
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
RefTypeParser().from_properties( RefTypeParser().from_properties(
"person", "person",
properties, properties,
@@ -110,7 +113,7 @@ class TestRefTypeParser(TestCase):
}, },
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
ObjectTypeParser().from_properties( ObjectTypeParser().from_properties(
"person", "person",
properties, properties,
@@ -126,7 +129,7 @@ class TestRefTypeParser(TestCase):
"$defs": {}, "$defs": {},
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
RefTypeParser().from_properties( RefTypeParser().from_properties(
"person", "person",
properties, properties,
@@ -142,7 +145,7 @@ class TestRefTypeParser(TestCase):
"$defs": {"person": None}, "$defs": {"person": None},
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
RefTypeParser().from_properties( RefTypeParser().from_properties(
"person", "person",
properties, properties,
@@ -232,7 +235,7 @@ class TestRefTypeParser(TestCase):
"required": ["name", "age"], "required": ["name", "age"],
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
ObjectTypeParser().from_properties( ObjectTypeParser().from_properties(
"person", "person",
properties, properties,
@@ -264,7 +267,7 @@ class TestRefTypeParser(TestCase):
) )
# checks if when created via FowardRef the model is validated correctly. # checks if when created via FowardRef the model is validated correctly.
with self.assertRaises(ValueError): with self.assertRaises(ValidationError):
model( model(
name="John", name="John",
age=30, 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( ref_strategy, ref_name, ref_property = RefTypeParser()._parse_from_strategy(
"invalid_strategy", "invalid_strategy",
"person", "person",

View File

@@ -1,3 +1,4 @@
from jambo.exceptions import InvalidSchemaException
from jambo.parser import StringTypeParser from jambo.parser import StringTypeParser
from pydantic import AnyUrl, EmailStr from pydantic import AnyUrl, EmailStr
@@ -62,7 +63,7 @@ class TestStringTypeParser(TestCase):
"minLength": 5, "minLength": 5,
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
def test_string_parser_with_default_invalid_maxlength(self): def test_string_parser_with_default_invalid_maxlength(self):
@@ -75,7 +76,7 @@ class TestStringTypeParser(TestCase):
"minLength": 1, "minLength": 1,
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
def test_string_parser_with_default_invalid_minlength(self): def test_string_parser_with_default_invalid_minlength(self):
@@ -88,7 +89,7 @@ class TestStringTypeParser(TestCase):
"minLength": 2, "minLength": 2,
} }
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
def test_string_parser_with_email_format(self): def test_string_parser_with_email_format(self):
@@ -183,11 +184,12 @@ class TestStringTypeParser(TestCase):
"format": "unsupported-format", "format": "unsupported-format",
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(InvalidSchemaException) as context:
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual( 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): 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 import StringTypeParser
from jambo.parser._type_parser import GenericTypeParser from jambo.parser._type_parser import GenericTypeParser
@@ -17,5 +18,5 @@ class TestGenericTypeParser(TestCase):
StringTypeParser.json_schema_type = "type:string" StringTypeParser.json_schema_type = "type:string"
def test_get_impl_invalid_type(self): def test_get_impl_invalid_type(self):
with self.assertRaises(ValueError): with self.assertRaises(InvalidSchemaException):
GenericTypeParser._get_impl({"type": "invalid_type"}) GenericTypeParser._get_impl({"type": "invalid_type"})

View File

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

79
uv.lock generated
View File

@@ -326,6 +326,7 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "coverage" }, { name = "coverage" },
{ name = "mypy" },
{ name = "poethepoet" }, { name = "poethepoet" },
{ name = "pre-commit" }, { name = "pre-commit" },
{ name = "ruff" }, { name = "ruff" },
@@ -333,6 +334,7 @@ dev = [
{ name = "sphinx", version = "8.2.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" },
{ name = "sphinx-rtd-theme" }, { name = "sphinx-rtd-theme" },
{ name = "types-jsonschema" },
] ]
[package.metadata] [package.metadata]
@@ -345,12 +347,14 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "coverage", specifier = ">=7.8.0" }, { name = "coverage", specifier = ">=7.8.0" },
{ name = "mypy", specifier = ">=1.18.1" },
{ name = "poethepoet", specifier = ">=0.33.1" }, { name = "poethepoet", specifier = ">=0.33.1" },
{ name = "pre-commit", specifier = ">=4.2.0" }, { name = "pre-commit", specifier = ">=4.2.0" },
{ name = "ruff", specifier = ">=0.11.4" }, { name = "ruff", specifier = ">=0.11.4" },
{ name = "sphinx", specifier = ">=8.1.3" }, { name = "sphinx", specifier = ">=8.1.3" },
{ name = "sphinx-autobuild", specifier = ">=2024.10.3" }, { name = "sphinx-autobuild", specifier = ">=2024.10.3" },
{ name = "sphinx-rtd-theme", specifier = ">=3.0.2" }, { name = "sphinx-rtd-theme", specifier = ">=3.0.2" },
{ name = "types-jsonschema", specifier = ">=4.25.1.20250822" },
] ]
[[package]] [[package]]
@@ -450,6 +454,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" }, { 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]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.9.1" version = "1.9.1"
@@ -477,6 +535,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" }, { 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]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.3.7" version = "4.3.7"
@@ -1048,6 +1115,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" }, { 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]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"