Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1b5e71a81 | |||
|
156c825a67
|
|||
| b4954c3b2e | |||
|
7f44e84bce
|
|||
|
8c6a04bbdf
|
|||
|
e31002af32
|
|||
|
30290771b1
|
|||
|
f4d84d2749
|
|||
|
e61d48881f
|
|||
| f5ad857326 | |||
|
e45086e29e
|
|||
|
c1f04606ad
|
|||
|
5eb086bafd
|
|||
| 5c30e752e3 | |||
|
53418f2b2b
|
|||
| 002b75c53a | |||
|
|
1167b8a540 | ||
| 3992057c95 | |||
|
71380073e4
|
|||
| 4055efa5bf | |||
|
97aed6e9aa
|
|||
|
d3a2f1e76c
|
|||
| 0a3671974f | |||
|
8761ee5ef6
|
|||
| 85b5900392 | |||
|
7e11c817a7
|
|||
| dc5853c5b2 | |||
|
1e5b686c23
|
|||
| bbe4c6979e | |||
|
c5e70402db
|
|||
|
15944549a0
|
|||
|
79932bb595
|
|||
|
86894fa918
|
|||
|
b386d4954e
|
|||
| 1cab13a4a0 | |||
|
6dad6e0c68
|
|||
|
fbbff0bd9e
|
|||
|
|
9aec7c3e3b
|
||
|
cc6f2d42d5
|
|||
|
|
9797fb35d9
|
||
| 81a5fffef0 | |||
|
00d88388f8
|
|||
| 609af7c32b | |||
| c59c1e8768 | |||
|
7b9464f458
|
|||
|
617f1aab2b
|
|||
|
|
976708934f | ||
|
|
e9d61a1268 |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -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:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Created to simplifying the process of dynamically generating Pydantic models for
|
|||||||
- nested objects
|
- nested objects
|
||||||
- allOf
|
- allOf
|
||||||
- anyOf
|
- anyOf
|
||||||
|
- oneOf
|
||||||
- ref
|
- ref
|
||||||
- enum
|
- enum
|
||||||
- const
|
- const
|
||||||
|
|||||||
37
docs/source/jambo.exceptions.rst
Normal file
37
docs/source/jambo.exceptions.rst
Normal 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:
|
||||||
@@ -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
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Subpackages
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|
||||||
|
jambo.exceptions
|
||||||
jambo.parser
|
jambo.parser
|
||||||
jambo.types
|
jambo.types
|
||||||
|
|
||||||
|
|||||||
112
docs/source/usage.oneof.rst
Normal file
112
docs/source/usage.oneof.rst
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
OneOf Type
|
||||||
|
=================
|
||||||
|
|
||||||
|
The OneOf type is used to specify that an object must conform to exactly one of the specified schemas. Unlike AnyOf which allows matching multiple schemas, OneOf enforces that the data matches one and only one of the provided schemas.
|
||||||
|
|
||||||
|
|
||||||
|
Examples
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
1. **Overlapping String Example** - A field that accepts strings with overlapping constraints:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from jambo import SchemaConverter
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
"title": "SimpleExample",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string", "maxLength": 6},
|
||||||
|
{"type": "string", "minLength": 4}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["value"]
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
# Valid: Short string (matches first schema only)
|
||||||
|
obj1 = Model(value="hi")
|
||||||
|
print(obj1.value) # Output: hi
|
||||||
|
|
||||||
|
# Valid: Long string (matches second schema only)
|
||||||
|
obj2 = Model(value="very long string")
|
||||||
|
print(obj2.value) # Output: very long string
|
||||||
|
|
||||||
|
# Invalid: Medium string (matches BOTH schemas - violates oneOf)
|
||||||
|
try:
|
||||||
|
obj3 = Model(value="hello") # 5 chars: matches maxLength=6 AND minLength=4
|
||||||
|
except ValueError as e:
|
||||||
|
print("Validation fails as expected:", e)
|
||||||
|
|
||||||
|
|
||||||
|
2. **Discriminator Example** - Different shapes with a type field:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from jambo import SchemaConverter
|
||||||
|
|
||||||
|
schema = {
|
||||||
|
"title": "Shape",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"shape": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "circle"},
|
||||||
|
"radius": {"type": "number", "minimum": 0}
|
||||||
|
},
|
||||||
|
"required": ["type", "radius"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "rectangle"},
|
||||||
|
"width": {"type": "number", "minimum": 0},
|
||||||
|
"height": {"type": "number", "minimum": 0}
|
||||||
|
},
|
||||||
|
"required": ["type", "width", "height"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"discriminator": {
|
||||||
|
"propertyName": "type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["shape"]
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
# Valid: Circle
|
||||||
|
circle = Model(shape={"type": "circle", "radius": 5.0})
|
||||||
|
print(circle.shape.type) # Output: circle
|
||||||
|
|
||||||
|
# Valid: Rectangle
|
||||||
|
rectangle = Model(shape={"type": "rectangle", "width": 10, "height": 20})
|
||||||
|
print(rectangle.shape.type) # Output: rectangle
|
||||||
|
|
||||||
|
# Invalid: Wrong properties for the type
|
||||||
|
try:
|
||||||
|
invalid = Model(shape={"type": "circle", "width": 10})
|
||||||
|
except ValueError as e:
|
||||||
|
print("Validation fails as expected:", e)
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
OneOf ensures exactly one schema matches. The discriminator helps Pydantic efficiently determine which schema to use based on a specific property value.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
If your data could match multiple schemas in a oneOf, validation will fail. Ensure schemas are mutually exclusive.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
The discriminator feature is not officially in the JSON Schema specification, it was introduced by OpenAPI. Use it with caution and ensure it fits your use case.
|
||||||
@@ -45,5 +45,6 @@ For more complex schemas and types see our documentation on
|
|||||||
usage.reference
|
usage.reference
|
||||||
usage.allof
|
usage.allof
|
||||||
usage.anyof
|
usage.anyof
|
||||||
|
usage.oneof
|
||||||
usage.enum
|
usage.enum
|
||||||
usage.const
|
usage.const
|
||||||
10
jambo/exceptions/__init__.py
Normal file
10
jambo/exceptions/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
16
jambo/exceptions/internal_assertion_exception.py
Normal file
16
jambo/exceptions/internal_assertion_exception.py
Normal 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"
|
||||||
|
)
|
||||||
27
jambo/exceptions/invalid_schema_exception.py
Normal file
27
jambo/exceptions/invalid_schema_exception.py
Normal 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
|
||||||
23
jambo/exceptions/unsupported_schema_exception.py
Normal file
23
jambo/exceptions/unsupported_schema_exception.py
Normal 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
|
||||||
@@ -7,7 +7,9 @@ from .const_type_parser import ConstTypeParser
|
|||||||
from .enum_type_parser import EnumTypeParser
|
from .enum_type_parser import EnumTypeParser
|
||||||
from .float_type_parser import FloatTypeParser
|
from .float_type_parser import FloatTypeParser
|
||||||
from .int_type_parser import IntTypeParser
|
from .int_type_parser import IntTypeParser
|
||||||
|
from .null_type_parser import NullTypeParser
|
||||||
from .object_type_parser import ObjectTypeParser
|
from .object_type_parser import ObjectTypeParser
|
||||||
|
from .oneof_type_parser import OneOfTypeParser
|
||||||
from .ref_type_parser import RefTypeParser
|
from .ref_type_parser import RefTypeParser
|
||||||
from .string_type_parser import StringTypeParser
|
from .string_type_parser import StringTypeParser
|
||||||
|
|
||||||
@@ -22,7 +24,9 @@ __all__ = [
|
|||||||
"BooleanTypeParser",
|
"BooleanTypeParser",
|
||||||
"FloatTypeParser",
|
"FloatTypeParser",
|
||||||
"IntTypeParser",
|
"IntTypeParser",
|
||||||
|
"NullTypeParser",
|
||||||
"ObjectTypeParser",
|
"ObjectTypeParser",
|
||||||
|
"OneOfTypeParser",
|
||||||
"StringTypeParser",
|
"StringTypeParser",
|
||||||
"RefTypeParser",
|
"RefTypeParser",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -35,7 +43,7 @@ class ArrayTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
||||||
|
|
||||||
if "default" not in mapped_properties:
|
if "default" in properties or not kwargs.get("required", False):
|
||||||
mapped_properties["default_factory"] = self._build_default_factory(
|
mapped_properties["default_factory"] = self._build_default_factory(
|
||||||
properties.get("default"), wrapper_type
|
properties.get("default"), wrapper_type
|
||||||
)
|
)
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
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
|
||||||
|
|
||||||
from pydantic import AfterValidator
|
from pydantic import AfterValidator
|
||||||
from typing_extensions import Annotated, Any, Unpack
|
from typing_extensions import Annotated, Any, Literal, Unpack
|
||||||
|
|
||||||
|
|
||||||
class ConstTypeParser(GenericTypeParser):
|
class ConstTypeParser(GenericTypeParser):
|
||||||
@@ -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)
|
||||||
@@ -33,6 +38,14 @@ class ConstTypeParser(GenericTypeParser):
|
|||||||
return const_type, parsed_properties
|
return const_type, parsed_properties
|
||||||
|
|
||||||
def _build_const_type(self, const_value):
|
def _build_const_type(self, const_value):
|
||||||
|
# Try to use Literal for hashable types (required for discriminated unions)
|
||||||
|
# Fall back to validator approach for non-hashable types
|
||||||
|
try:
|
||||||
|
# Test if the value is hashable (can be used in Literal)
|
||||||
|
hash(const_value)
|
||||||
|
return Literal[const_value]
|
||||||
|
except TypeError:
|
||||||
|
# Non-hashable type (like list, dict), use validator approach
|
||||||
def _validate_const_value(value: Any) -> Any:
|
def _validate_const_value(value: Any) -> Any:
|
||||||
if value != const_value:
|
if value != const_value:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
18
jambo/parser/null_type_parser.py
Normal file
18
jambo/parser/null_type_parser.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
from jambo.types.type_parser_options import TypeParserOptions
|
||||||
|
|
||||||
|
from typing_extensions import Unpack
|
||||||
|
|
||||||
|
|
||||||
|
class NullTypeParser(GenericTypeParser):
|
||||||
|
mapped_type = type(None)
|
||||||
|
|
||||||
|
json_schema_type = "type:null"
|
||||||
|
|
||||||
|
def from_properties_impl(
|
||||||
|
self, name, properties, **kwargs: Unpack[TypeParserOptions]
|
||||||
|
):
|
||||||
|
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
||||||
|
mapped_properties["default"] = None
|
||||||
|
|
||||||
|
return self.mapped_type, mapped_properties
|
||||||
@@ -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,38 +34,40 @@ 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 = {}
|
||||||
for name, prop in properties.items():
|
for name, prop in properties.items():
|
||||||
sub_property = kwargs.copy()
|
sub_property: TypeParserOptions = kwargs.copy()
|
||||||
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))
|
||||||
|
|
||||||
|
|||||||
115
jambo/parser/oneof_type_parser.py
Normal file
115
jambo/parser/oneof_type_parser.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
from jambo.exceptions import InvalidSchemaException
|
||||||
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
|
from jambo.types.type_parser_options import TypeParserOptions
|
||||||
|
|
||||||
|
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, ValidationError
|
||||||
|
from typing_extensions import Annotated, Any, Union, Unpack, get_args
|
||||||
|
|
||||||
|
|
||||||
|
Annotation = Annotated[Any, ...]
|
||||||
|
|
||||||
|
|
||||||
|
class OneOfTypeParser(GenericTypeParser):
|
||||||
|
mapped_type = Union
|
||||||
|
|
||||||
|
json_schema_type = "oneOf"
|
||||||
|
|
||||||
|
def from_properties_impl(
|
||||||
|
self, name, properties, **kwargs: Unpack[TypeParserOptions]
|
||||||
|
):
|
||||||
|
if "oneOf" not in properties:
|
||||||
|
raise InvalidSchemaException(
|
||||||
|
f"Invalid JSON Schema: {properties}", invalid_field="oneOf"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(properties["oneOf"], list) or len(properties["oneOf"]) == 0:
|
||||||
|
raise InvalidSchemaException(
|
||||||
|
f"Invalid JSON Schema: {properties['oneOf']}", invalid_field="oneOf"
|
||||||
|
)
|
||||||
|
|
||||||
|
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
||||||
|
|
||||||
|
sub_properties = properties["oneOf"]
|
||||||
|
|
||||||
|
sub_types = [
|
||||||
|
GenericTypeParser.type_from_properties(name, subProperty, **kwargs)
|
||||||
|
for subProperty in sub_properties
|
||||||
|
]
|
||||||
|
|
||||||
|
if not kwargs.get("required", False):
|
||||||
|
mapped_properties["default"] = mapped_properties.get("default")
|
||||||
|
|
||||||
|
subfield_types = [Annotated[t, Field(**v)] for t, v in sub_types]
|
||||||
|
|
||||||
|
# Added with the understanding of discriminator are not in the JsonSchema Spec,
|
||||||
|
# they were added by OpenAPI and not all implementations may support them,
|
||||||
|
# and they do not always generate a model one-to-one to the Pydantic model
|
||||||
|
# TL;DR: Discriminators were added by OpenAPI and not a Official JSON Schema feature
|
||||||
|
discriminator = properties.get("discriminator")
|
||||||
|
if discriminator is not None:
|
||||||
|
validated_type = self._build_type_one_of_with_discriminator(
|
||||||
|
subfield_types, discriminator
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
validated_type = self._build_type_one_of_with_func(subfield_types)
|
||||||
|
|
||||||
|
return validated_type, mapped_properties
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_type_one_of_with_discriminator(
|
||||||
|
subfield_types: list[Annotation], discriminator_prop: dict
|
||||||
|
) -> Annotation:
|
||||||
|
"""
|
||||||
|
Build a type with a discriminator.
|
||||||
|
"""
|
||||||
|
if not isinstance(discriminator_prop, dict):
|
||||||
|
raise InvalidSchemaException(
|
||||||
|
"Discriminator must be a dictionary", invalid_field="discriminator"
|
||||||
|
)
|
||||||
|
|
||||||
|
for field in subfield_types:
|
||||||
|
field_type, field_info = get_args(field)
|
||||||
|
|
||||||
|
if issubclass(field_type, BaseModel):
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise InvalidSchemaException(
|
||||||
|
"When using a discriminator, all subfield types must be of type 'object'.",
|
||||||
|
invalid_field="discriminator",
|
||||||
|
)
|
||||||
|
|
||||||
|
property_name = discriminator_prop.get("propertyName")
|
||||||
|
if property_name is None or not isinstance(property_name, str):
|
||||||
|
raise InvalidSchemaException(
|
||||||
|
"Discriminator must have a 'propertyName' key",
|
||||||
|
invalid_field="propertyName",
|
||||||
|
)
|
||||||
|
|
||||||
|
return Annotated[Union[(*subfield_types,)], Field(discriminator=property_name)]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_type_one_of_with_func(subfield_types: list[Annotation]) -> Annotation:
|
||||||
|
"""
|
||||||
|
Build a type with a validation function for the oneOf constraint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def validate_one_of(value: Any) -> Any:
|
||||||
|
matched_count = 0
|
||||||
|
|
||||||
|
for field_type in subfield_types:
|
||||||
|
try:
|
||||||
|
TypeAdapter(field_type).validate_python(value)
|
||||||
|
matched_count += 1
|
||||||
|
except ValidationError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if matched_count == 0:
|
||||||
|
raise ValueError("Value does not match any of the oneOf schemas")
|
||||||
|
elif matched_count > 1:
|
||||||
|
raise ValueError(
|
||||||
|
"Value matches multiple oneOf schemas, exactly one expected"
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
return Annotated[Union[(*subfield_types,)], BeforeValidator(validate_one_of)]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
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
|
||||||
|
|
||||||
from pydantic import EmailStr, HttpUrl, IPvAnyAddress
|
from pydantic import AnyUrl, EmailStr
|
||||||
from typing_extensions import Unpack
|
from typing_extensions import Unpack
|
||||||
|
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time, timedelta
|
||||||
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
class StringTypeParser(GenericTypeParser):
|
class StringTypeParser(GenericTypeParser):
|
||||||
@@ -20,14 +23,22 @@ class StringTypeParser(GenericTypeParser):
|
|||||||
}
|
}
|
||||||
|
|
||||||
format_type_mapping = {
|
format_type_mapping = {
|
||||||
"email": EmailStr,
|
# [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
|
||||||
"uri": HttpUrl,
|
|
||||||
"ipv4": IPvAnyAddress,
|
|
||||||
"ipv6": IPvAnyAddress,
|
|
||||||
"hostname": str,
|
|
||||||
"date": date,
|
"date": date,
|
||||||
"time": time,
|
"time": time,
|
||||||
"date-time": datetime,
|
"date-time": datetime,
|
||||||
|
"duration": timedelta,
|
||||||
|
# [7.3.2](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.2). Email Addresses
|
||||||
|
"email": EmailStr,
|
||||||
|
# [7.3.3](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.3). Hostnames
|
||||||
|
"hostname": str,
|
||||||
|
# [7.3.4](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.4). IP Addresses
|
||||||
|
"ipv4": IPv4Address,
|
||||||
|
"ipv6": IPv6Address,
|
||||||
|
# [7.3.5](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.5). Resource Identifiers
|
||||||
|
"uri": AnyUrl,
|
||||||
|
# "iri" # Not supported by pydantic and currently not supported by jambo
|
||||||
|
"uuid": UUID,
|
||||||
}
|
}
|
||||||
|
|
||||||
format_pattern_mapping = {
|
format_pattern_mapping = {
|
||||||
@@ -37,16 +48,16 @@ class StringTypeParser(GenericTypeParser):
|
|||||||
def from_properties_impl(
|
def from_properties_impl(
|
||||||
self, name, properties, **kwargs: Unpack[TypeParserOptions]
|
self, name, properties, **kwargs: Unpack[TypeParserOptions]
|
||||||
):
|
):
|
||||||
mapped_properties = self.mappings_properties_builder(
|
mapped_properties = self.mappings_properties_builder(properties, **kwargs)
|
||||||
properties, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
format_type = properties.get("format")
|
format_type = properties.get("format")
|
||||||
if not format_type:
|
if not format_type:
|
||||||
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
0
jambo/py.typed
Normal 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")
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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,6 +52,7 @@ 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"
|
||||||
|
type-check = "mypy jambo"
|
||||||
serve-docs = "sphinx-autobuild docs/source docs/build"
|
serve-docs = "sphinx-autobuild docs/source docs/build"
|
||||||
|
|
||||||
# Build System
|
# Build System
|
||||||
|
|||||||
0
tests/exceptions/__init__.py
Normal file
0
tests/exceptions/__init__.py
Normal file
21
tests/exceptions/test_internal_assertion_exception.py
Normal file
21
tests/exceptions/test_internal_assertion_exception.py
Normal 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)
|
||||||
44
tests/exceptions/test_invalid_schema_exception.py
Normal file
44
tests/exceptions/test_invalid_schema_exception.py
Normal 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)
|
||||||
31
tests/exceptions/test_unsupported_schema_exception.py
Normal file
31
tests/exceptions/test_unsupported_schema_exception.py
Normal 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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
|
from jambo.exceptions import InvalidSchemaException
|
||||||
from jambo.parser import ConstTypeParser
|
from jambo.parser import ConstTypeParser
|
||||||
|
|
||||||
from typing_extensions import Annotated, get_args, get_origin
|
from typing_extensions import Annotated, Literal, get_args, get_origin
|
||||||
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
class TestConstTypeParser(TestCase):
|
class TestConstTypeParser(TestCase):
|
||||||
def test_const_type_parser(self):
|
def test_const_type_parser_hashable_value(self):
|
||||||
|
"""Test const parser with hashable values (uses Literal)"""
|
||||||
parser = ConstTypeParser()
|
parser = ConstTypeParser()
|
||||||
|
|
||||||
expected_const_value = "United States of America"
|
expected_const_value = "United States of America"
|
||||||
@@ -16,8 +18,60 @@ class TestConstTypeParser(TestCase):
|
|||||||
"country", properties
|
"country", properties
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check that we get a Literal type for hashable values
|
||||||
|
self.assertEqual(get_origin(parsed_type), Literal)
|
||||||
|
self.assertEqual(get_args(parsed_type), (expected_const_value,))
|
||||||
|
|
||||||
|
self.assertEqual(parsed_properties["default"], expected_const_value)
|
||||||
|
|
||||||
|
def test_const_type_parser_non_hashable_value(self):
|
||||||
|
"""Test const parser with non-hashable values (uses Annotated with validator)"""
|
||||||
|
parser = ConstTypeParser()
|
||||||
|
|
||||||
|
expected_const_value = [1, 2, 3] # Lists are not hashable
|
||||||
|
properties = {"const": expected_const_value}
|
||||||
|
|
||||||
|
parsed_type, parsed_properties = parser.from_properties_impl(
|
||||||
|
"list_const", properties
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that we get an Annotated type for non-hashable values
|
||||||
self.assertEqual(get_origin(parsed_type), Annotated)
|
self.assertEqual(get_origin(parsed_type), Annotated)
|
||||||
self.assertIn(str, get_args(parsed_type))
|
self.assertIn(list, get_args(parsed_type))
|
||||||
|
|
||||||
|
self.assertEqual(parsed_properties["default"], expected_const_value)
|
||||||
|
|
||||||
|
def test_const_type_parser_integer_value(self):
|
||||||
|
"""Test const parser with integer values (uses Literal)"""
|
||||||
|
parser = ConstTypeParser()
|
||||||
|
|
||||||
|
expected_const_value = 42
|
||||||
|
properties = {"const": expected_const_value}
|
||||||
|
|
||||||
|
parsed_type, parsed_properties = parser.from_properties_impl(
|
||||||
|
"int_const", properties
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that we get a Literal type for hashable values
|
||||||
|
self.assertEqual(get_origin(parsed_type), Literal)
|
||||||
|
self.assertEqual(get_args(parsed_type), (expected_const_value,))
|
||||||
|
|
||||||
|
self.assertEqual(parsed_properties["default"], expected_const_value)
|
||||||
|
|
||||||
|
def test_const_type_parser_boolean_value(self):
|
||||||
|
"""Test const parser with boolean values (uses Literal)"""
|
||||||
|
parser = ConstTypeParser()
|
||||||
|
|
||||||
|
expected_const_value = True
|
||||||
|
properties = {"const": expected_const_value}
|
||||||
|
|
||||||
|
parsed_type, parsed_properties = parser.from_properties_impl(
|
||||||
|
"bool_const", properties
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that we get a Literal type for hashable values
|
||||||
|
self.assertEqual(get_origin(parsed_type), Literal)
|
||||||
|
self.assertEqual(get_args(parsed_type), (expected_const_value,))
|
||||||
|
|
||||||
self.assertEqual(parsed_properties["default"], expected_const_value)
|
self.assertEqual(parsed_properties["default"], expected_const_value)
|
||||||
|
|
||||||
@@ -27,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(
|
||||||
@@ -40,7 +94,7 @@ 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(
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -86,5 +87,5 @@ class TestEnumTypeParser(TestCase):
|
|||||||
"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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
29
tests/parser/test_null_type_parser.py
Normal file
29
tests/parser/test_null_type_parser.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from jambo.parser import NullTypeParser
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestNullTypeParser(TestCase):
|
||||||
|
def test_null_parser_no_options(self):
|
||||||
|
parser = NullTypeParser()
|
||||||
|
|
||||||
|
properties = {"type": "null"}
|
||||||
|
|
||||||
|
type_parsing, type_validator = parser.from_properties_impl(
|
||||||
|
"placeholder", properties
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(type_parsing, type(None))
|
||||||
|
self.assertEqual(type_validator, {"default": None})
|
||||||
|
|
||||||
|
def test_null_parser_with_invalid_default(self):
|
||||||
|
parser = NullTypeParser()
|
||||||
|
|
||||||
|
properties = {"type": "null", "default": "invalid"}
|
||||||
|
|
||||||
|
type_parsing, type_validator = parser.from_properties_impl(
|
||||||
|
"placeholder", properties
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(type_parsing, type(None))
|
||||||
|
self.assertEqual(type_validator, {"default": None})
|
||||||
534
tests/parser/test_oneof_type_parser.py
Normal file
534
tests/parser/test_oneof_type_parser.py
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
from jambo import SchemaConverter
|
||||||
|
from jambo.exceptions import InvalidSchemaException
|
||||||
|
from jambo.parser.oneof_type_parser import OneOfTypeParser
|
||||||
|
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestOneOfTypeParser(TestCase):
|
||||||
|
def test_oneof_raises_on_invalid_property(self):
|
||||||
|
with self.assertRaises(InvalidSchemaException):
|
||||||
|
OneOfTypeParser().from_properties_impl(
|
||||||
|
"test_field",
|
||||||
|
{
|
||||||
|
# Invalid schema, should have property "oneOf"
|
||||||
|
},
|
||||||
|
required=True,
|
||||||
|
context={},
|
||||||
|
ref_cache={},
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidSchemaException):
|
||||||
|
OneOfTypeParser().from_properties_impl(
|
||||||
|
"test_field",
|
||||||
|
{
|
||||||
|
"oneOf": [], # should throw because oneOf must be a list with at least one item
|
||||||
|
},
|
||||||
|
required=True,
|
||||||
|
context={},
|
||||||
|
ref_cache={},
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidSchemaException):
|
||||||
|
SchemaConverter.build(
|
||||||
|
{
|
||||||
|
"title": "Test",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"oneOf": [], # should throw because oneOf requires at least one schema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_oneof_basic_integer_and_string(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Person",
|
||||||
|
"description": "A person with an ID that can be either an integer or a formatted string",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "integer", "minimum": 1},
|
||||||
|
{"type": "string", "pattern": "^[A-Z]{2}[0-9]{4}$"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
obj1 = Model(id=123)
|
||||||
|
self.assertEqual(obj1.id, 123)
|
||||||
|
|
||||||
|
obj2 = Model(id="AB1234")
|
||||||
|
self.assertEqual(obj2.id, "AB1234")
|
||||||
|
|
||||||
|
def test_oneof_validation_failures(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "integer", "minimum": 1},
|
||||||
|
{"type": "string", "pattern": "^[A-Z]{2}[0-9]{4}$"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(id=-5)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(id="invalid")
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(id=123.45)
|
||||||
|
|
||||||
|
def test_oneof_with_conflicting_schemas(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Value",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "number", "multipleOf": 2},
|
||||||
|
{"type": "number", "multipleOf": 3},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["data"],
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
obj1 = Model(data=4)
|
||||||
|
self.assertEqual(obj1.data, 4)
|
||||||
|
|
||||||
|
obj2 = Model(data=9)
|
||||||
|
self.assertEqual(obj2.data, 9)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError) as cm:
|
||||||
|
Model(data=6)
|
||||||
|
self.assertIn("matches multiple oneOf schemas", str(cm.exception))
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(data=5)
|
||||||
|
|
||||||
|
def test_oneof_with_objects(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Contact",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"contact_info": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {"type": "string", "format": "email"}
|
||||||
|
},
|
||||||
|
"required": ["email"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"phone": {"type": "string", "pattern": "^[0-9-]+$"}
|
||||||
|
},
|
||||||
|
"required": ["phone"],
|
||||||
|
"additionalProperties": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["contact_info"],
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
obj1 = Model(contact_info={"email": "user@example.com"})
|
||||||
|
self.assertEqual(obj1.contact_info.email, "user@example.com")
|
||||||
|
|
||||||
|
obj2 = Model(contact_info={"phone": "123-456-7890"})
|
||||||
|
self.assertEqual(obj2.contact_info.phone, "123-456-7890")
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(contact_info={"email": "user@example.com", "phone": "123-456-7890"})
|
||||||
|
|
||||||
|
def test_oneof_with_discriminator_basic(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Pet",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pet": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "cat"},
|
||||||
|
"meows": {"type": "boolean"},
|
||||||
|
},
|
||||||
|
"required": ["type", "meows"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "dog"},
|
||||||
|
"barks": {"type": "boolean"},
|
||||||
|
},
|
||||||
|
"required": ["type", "barks"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"discriminator": {"propertyName": "type"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["pet"],
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
cat = Model(pet={"type": "cat", "meows": True})
|
||||||
|
self.assertEqual(cat.pet.type, "cat")
|
||||||
|
self.assertEqual(cat.pet.meows, True)
|
||||||
|
|
||||||
|
dog = Model(pet={"type": "dog", "barks": False})
|
||||||
|
self.assertEqual(dog.pet.type, "dog")
|
||||||
|
self.assertEqual(dog.pet.barks, False)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(pet={"type": "cat", "barks": True})
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(pet={"type": "bird", "flies": True})
|
||||||
|
|
||||||
|
def test_oneof_with_invalid_types(self):
|
||||||
|
with self.assertRaises(InvalidSchemaException):
|
||||||
|
SchemaConverter.build(
|
||||||
|
{
|
||||||
|
"title": "Pet",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pet": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"discriminator": {"propertyName": "type"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["pet"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_oneof_with_discriminator_mapping(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Vehicle",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"vehicle": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"vehicle_type": {"const": "car"},
|
||||||
|
"doors": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 2,
|
||||||
|
"maximum": 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["vehicle_type", "doors"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"vehicle_type": {"const": "motorcycle"},
|
||||||
|
"engine_size": {"type": "number", "minimum": 125},
|
||||||
|
},
|
||||||
|
"required": ["vehicle_type", "engine_size"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"discriminator": {
|
||||||
|
"propertyName": "vehicle_type",
|
||||||
|
"mapping": {
|
||||||
|
"car": "#/properties/vehicle/oneOf/0",
|
||||||
|
"motorcycle": "#/properties/vehicle/oneOf/1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["vehicle"],
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
car = Model(vehicle={"vehicle_type": "car", "doors": 4})
|
||||||
|
self.assertEqual(car.vehicle.vehicle_type, "car")
|
||||||
|
self.assertEqual(car.vehicle.doors, 4)
|
||||||
|
|
||||||
|
motorcycle = Model(vehicle={"vehicle_type": "motorcycle", "engine_size": 600.0})
|
||||||
|
self.assertEqual(motorcycle.vehicle.vehicle_type, "motorcycle")
|
||||||
|
self.assertEqual(motorcycle.vehicle.engine_size, 600.0)
|
||||||
|
|
||||||
|
def test_oneof_with_discriminator_invalid_values(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Shape",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"shape": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "circle"},
|
||||||
|
"radius": {"type": "number", "minimum": 0},
|
||||||
|
},
|
||||||
|
"required": ["type", "radius"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "square"},
|
||||||
|
"side": {"type": "number", "minimum": 0},
|
||||||
|
},
|
||||||
|
"required": ["type", "side"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"discriminator": {"propertyName": "type"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["shape"],
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(shape={"type": "triangle", "base": 5, "height": 3})
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(shape={"type": "circle", "side": 5})
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(shape={"radius": 5})
|
||||||
|
|
||||||
|
def test_oneof_missing_properties(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Test",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"notOneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "integer"},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidSchemaException):
|
||||||
|
SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
def test_oneof_invalid_properties(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Test",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {"oneOf": None},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidSchemaException):
|
||||||
|
SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
def test_oneof_with_default_value(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Test",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{"type": "integer"},
|
||||||
|
],
|
||||||
|
"default": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
obj = Model()
|
||||||
|
self.assertEqual(obj.value, "test")
|
||||||
|
|
||||||
|
def test_oneof_with_invalid_default_value(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Test",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string", "minLength": 5},
|
||||||
|
{"type": "integer", "minimum": 10},
|
||||||
|
],
|
||||||
|
"default": "hi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidSchemaException):
|
||||||
|
SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
def test_oneof_discriminator_without_property_name(self):
|
||||||
|
# Should throw because the spec determines propertyName is required for discriminator
|
||||||
|
with self.assertRaises(InvalidSchemaException):
|
||||||
|
SchemaConverter.build(
|
||||||
|
{
|
||||||
|
"title": "Test",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "a"},
|
||||||
|
"value": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "b"},
|
||||||
|
"value": {"type": "integer"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"discriminator": {}, # discriminator without propertyName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_oneof_discriminator_with_invalid_discriminator(self):
|
||||||
|
# Should throw because a valid discriminator is required
|
||||||
|
with self.assertRaises(InvalidSchemaException):
|
||||||
|
SchemaConverter.build(
|
||||||
|
{
|
||||||
|
"title": "Test",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "a"},
|
||||||
|
"value": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "b"},
|
||||||
|
"value": {"type": "integer"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"discriminator": "invalid", # discriminator without propertyName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_oneof_overlapping_strings_from_docs(self):
|
||||||
|
"""Test the overlapping strings example from documentation"""
|
||||||
|
schema = {
|
||||||
|
"title": "SimpleExample",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string", "maxLength": 6},
|
||||||
|
{"type": "string", "minLength": 4},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["value"],
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
# Valid: Short string (matches first schema only)
|
||||||
|
obj1 = Model(value="hi")
|
||||||
|
self.assertEqual(obj1.value, "hi")
|
||||||
|
|
||||||
|
# Valid: Long string (matches second schema only)
|
||||||
|
obj2 = Model(value="very long string")
|
||||||
|
self.assertEqual(obj2.value, "very long string")
|
||||||
|
|
||||||
|
# Invalid: Medium string (matches BOTH schemas - violates oneOf)
|
||||||
|
with self.assertRaises(ValidationError) as cm:
|
||||||
|
Model(value="hello") # 5 chars: matches maxLength=6 AND minLength=4
|
||||||
|
|
||||||
|
self.assertIn("matches multiple oneOf schemas", str(cm.exception))
|
||||||
|
|
||||||
|
def test_oneof_shapes_discriminator_from_docs(self):
|
||||||
|
"""Test the shapes discriminator example from documentation"""
|
||||||
|
schema = {
|
||||||
|
"title": "Shape",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"shape": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "circle"},
|
||||||
|
"radius": {"type": "number", "minimum": 0},
|
||||||
|
},
|
||||||
|
"required": ["type", "radius"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": "rectangle"},
|
||||||
|
"width": {"type": "number", "minimum": 0},
|
||||||
|
"height": {"type": "number", "minimum": 0},
|
||||||
|
},
|
||||||
|
"required": ["type", "width", "height"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"discriminator": {"propertyName": "type"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["shape"],
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
# Valid: Circle
|
||||||
|
circle = Model(shape={"type": "circle", "radius": 5.0})
|
||||||
|
self.assertEqual(circle.shape.type, "circle")
|
||||||
|
self.assertEqual(circle.shape.radius, 5.0)
|
||||||
|
|
||||||
|
# Valid: Rectangle
|
||||||
|
rectangle = Model(shape={"type": "rectangle", "width": 10, "height": 20})
|
||||||
|
self.assertEqual(rectangle.shape.type, "rectangle")
|
||||||
|
self.assertEqual(rectangle.shape.width, 10)
|
||||||
|
self.assertEqual(rectangle.shape.height, 20)
|
||||||
|
|
||||||
|
# Invalid: Wrong properties for the type
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(shape={"type": "circle", "width": 10})
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
from jambo.exceptions import InvalidSchemaException
|
||||||
from jambo.parser import StringTypeParser
|
from jambo.parser import StringTypeParser
|
||||||
|
|
||||||
from pydantic import EmailStr, HttpUrl, IPvAnyAddress
|
from pydantic import AnyUrl, EmailStr
|
||||||
|
|
||||||
from datetime import date, datetime, time
|
from datetime import date, datetime, time, timedelta
|
||||||
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
class TestStringTypeParser(TestCase):
|
class TestStringTypeParser(TestCase):
|
||||||
@@ -60,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):
|
||||||
@@ -73,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):
|
||||||
@@ -86,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):
|
||||||
@@ -111,12 +114,14 @@ class TestStringTypeParser(TestCase):
|
|||||||
|
|
||||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(type_parsing, HttpUrl)
|
self.assertEqual(type_parsing, AnyUrl)
|
||||||
|
|
||||||
def test_string_parser_with_ip_formats(self):
|
def test_string_parser_with_ip_formats(self):
|
||||||
parser = StringTypeParser()
|
parser = StringTypeParser()
|
||||||
|
|
||||||
for ip_format in ["ipv4", "ipv6"]:
|
formats = {"ipv4": IPv4Address, "ipv6": IPv6Address}
|
||||||
|
|
||||||
|
for ip_format, expected_type in formats.items():
|
||||||
properties = {
|
properties = {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": ip_format,
|
"format": ip_format,
|
||||||
@@ -126,7 +131,19 @@ class TestStringTypeParser(TestCase):
|
|||||||
"placeholder", properties
|
"placeholder", properties
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(type_parsing, IPvAnyAddress)
|
self.assertEqual(type_parsing, expected_type)
|
||||||
|
|
||||||
|
def test_string_parser_with_uuid_format(self):
|
||||||
|
parser = StringTypeParser()
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
|
self.assertEqual(type_parsing, UUID)
|
||||||
|
|
||||||
def test_string_parser_with_time_format(self):
|
def test_string_parser_with_time_format(self):
|
||||||
parser = StringTypeParser()
|
parser = StringTypeParser()
|
||||||
@@ -167,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):
|
||||||
@@ -197,3 +215,15 @@ class TestStringTypeParser(TestCase):
|
|||||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(type_parsing, datetime)
|
self.assertEqual(type_parsing, datetime)
|
||||||
|
|
||||||
|
def test_string_parser_with_timedelta_format(self):
|
||||||
|
parser = StringTypeParser()
|
||||||
|
|
||||||
|
properties = {
|
||||||
|
"type": "string",
|
||||||
|
"format": "duration",
|
||||||
|
}
|
||||||
|
|
||||||
|
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||||
|
|
||||||
|
self.assertEqual(type_parsing, timedelta)
|
||||||
|
|||||||
@@ -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"})
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
from jambo import SchemaConverter
|
from jambo import SchemaConverter
|
||||||
|
from jambo.exceptions import InvalidSchemaException, UnsupportedSchemaException
|
||||||
|
|
||||||
from pydantic import BaseModel, HttpUrl
|
from pydantic import AnyUrl, BaseModel, ValidationError
|
||||||
|
|
||||||
from ipaddress import IPv4Address, IPv6Address
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
def is_pydantic_model(cls):
|
def is_pydantic_model(cls):
|
||||||
@@ -22,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):
|
||||||
@@ -35,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):
|
||||||
@@ -45,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):
|
||||||
@@ -61,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))
|
||||||
|
|
||||||
@@ -102,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):
|
||||||
@@ -133,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):
|
||||||
@@ -158,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):
|
||||||
@@ -181,7 +197,7 @@ class TestSchemaConverter(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(model(is_active="true").is_active, True)
|
self.assertEqual(model(is_active="true").is_active, True)
|
||||||
|
|
||||||
def test_validation_list(self):
|
def test_validation_list_with_valid_items(self):
|
||||||
schema = {
|
schema = {
|
||||||
"title": "Person",
|
"title": "Person",
|
||||||
"description": "A person",
|
"description": "A person",
|
||||||
@@ -204,12 +220,52 @@ 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):
|
||||||
|
model = SchemaConverter.build(
|
||||||
|
{
|
||||||
|
"title": "Person",
|
||||||
|
"description": "A person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"friends": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 2,
|
||||||
|
"default": ["John", "Jane"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(model().friends, ["John", "Jane"])
|
||||||
|
|
||||||
|
model = SchemaConverter.build(
|
||||||
|
{
|
||||||
|
"title": "Person",
|
||||||
|
"description": "A person",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"friends": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["friends"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
model()
|
||||||
|
|
||||||
def test_validation_object(self):
|
def test_validation_object(self):
|
||||||
schema = {
|
schema = {
|
||||||
"title": "Person",
|
"title": "Person",
|
||||||
@@ -235,6 +291,9 @@ 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(ValidationError):
|
||||||
|
model()
|
||||||
|
|
||||||
def test_default_for_string(self):
|
def test_default_for_string(self):
|
||||||
schema = {
|
schema = {
|
||||||
"title": "Person",
|
"title": "Person",
|
||||||
@@ -271,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):
|
||||||
@@ -363,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):
|
||||||
@@ -392,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):
|
||||||
@@ -407,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):
|
||||||
@@ -418,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, HttpUrl("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):
|
||||||
@@ -431,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):
|
||||||
@@ -442,23 +507,44 @@ 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):
|
||||||
|
schema = {
|
||||||
|
"title": "UUIDTest",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"id": {"type": "string", "format": "uuid"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
model(id="123e4567-e89b-12d3-a456-426614174000").id,
|
||||||
|
UUID("123e4567-e89b-12d3-a456-426614174000"),
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
model(id="invalid-uuid")
|
||||||
|
|
||||||
def test_string_format_hostname(self):
|
def test_string_format_hostname(self):
|
||||||
schema = {
|
schema = {
|
||||||
"title": "HostnameTest",
|
"title": "HostnameTest",
|
||||||
"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):
|
||||||
@@ -467,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):
|
||||||
@@ -481,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):
|
||||||
@@ -494,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):
|
||||||
@@ -652,8 +743,51 @@ 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):
|
||||||
|
schema = {
|
||||||
|
"title": "Country",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"const": ["Brazil"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name"],
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
obj = Model()
|
||||||
|
self.assertEqual(obj.name, ["Brazil"])
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
obj.name = ["Argentina"]
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(name=["Argentina"])
|
||||||
|
|
||||||
|
def test_null_type_parser(self):
|
||||||
|
schema = {
|
||||||
|
"title": "Test",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"a_thing": {"type": "null"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Model = SchemaConverter.build(schema)
|
||||||
|
|
||||||
|
obj = Model()
|
||||||
|
self.assertIsNone(obj.a_thing)
|
||||||
|
|
||||||
|
obj = Model(a_thing=None)
|
||||||
|
self.assertIsNone(obj.a_thing)
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
Model(a_thing="not none")
|
||||||
|
|||||||
79
uv.lock
generated
79
uv.lock
generated
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user