From 936c5b350acc1104593b5de7418bbb7846db5eee Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Mon, 7 Apr 2025 19:23:49 -0300 Subject: [PATCH 1/8] Initial Fields Validators Implementation --- jambo/schema_converter.py | 40 ++++++++++++++++++++++++---------- jambo/types/_type_parser.py | 4 +++- jambo/types/int_type_parser.py | 15 ++++++++++++- pyproject.toml | 2 +- tests/test_type_parser.py | 5 +++-- 5 files changed, 50 insertions(+), 16 deletions(-) diff --git a/jambo/schema_converter.py b/jambo/schema_converter.py index e2f4fe9..e69d1b8 100644 --- a/jambo/schema_converter.py +++ b/jambo/schema_converter.py @@ -9,17 +9,23 @@ from typing import Type class SchemaConverter: - @staticmethod - def build(schema): - try: - Validator.check_schema(schema) - except SchemaError as e: - raise ValueError(f"Invalid JSON Schema: {e}") + """ + Converts JSON Schema to Pydantic models. - if schema["type"] != "object": - raise TypeError( - f"Invalid JSON Schema: {schema['type']}. Only 'object' can be converted to Pydantic models." - ) + This class is responsible for converting JSON Schema definitions into Pydantic models. + It validates the schema and generates the corresponding Pydantic model with appropriate + fields and types. The generated model can be used for data validation and serialization. + """ + + @staticmethod + def build(schema: dict) -> Type: + """ + Converts a JSON Schema to a Pydantic model. + :param schema: The JSON Schema to convert. + :return: A Pydantic model class. + """ + if "title" not in schema: + raise ValueError("JSON Schema must have a title.") return SchemaConverter.build_object(schema["title"], schema) @@ -27,7 +33,19 @@ class SchemaConverter: def build_object( name: str, schema: dict, - ): + ) -> Type: + """ + Converts a JSON Schema object to a Pydantic model given a name. + :param name: + :param schema: + :return: + """ + + try: + Validator.check_schema(schema) + except SchemaError as e: + raise ValueError(f"Invalid JSON Schema: {e}") + if schema["type"] != "object": raise TypeError( f"Invalid JSON Schema: {schema['type']}. Only 'object' can be converted to Pydantic models." diff --git a/jambo/types/_type_parser.py b/jambo/types/_type_parser.py index 2a76d01..a2ec13f 100644 --- a/jambo/types/_type_parser.py +++ b/jambo/types/_type_parser.py @@ -1,6 +1,8 @@ from abc import ABC, abstractmethod from typing import Generic, Self, TypeVar +from pydantic import Field + T = TypeVar("T") @@ -17,7 +19,7 @@ class GenericTypeParser(ABC, Generic[T]): @abstractmethod def from_properties( name: str, properties: dict[str, any] - ) -> tuple[type[T], dict[str, any]]: ... + ) -> tuple[type[T], Field]: ... @classmethod def get_impl(cls, type_name: str) -> Self: diff --git a/jambo/types/int_type_parser.py b/jambo/types/int_type_parser.py index 121bdd4..4fa9778 100644 --- a/jambo/types/int_type_parser.py +++ b/jambo/types/int_type_parser.py @@ -1,3 +1,5 @@ +from dataclasses import Field + from jambo.types._type_parser import GenericTypeParser @@ -8,4 +10,15 @@ class IntTypeParser(GenericTypeParser): @staticmethod def from_properties(name, properties): - return int, {} + _field_properties = dict() + + if "minimum" in properties: + _field_properties["ge"] = properties["minimum"] + + if "maximum" in properties: + _field_properties["le"] = properties["maximum"] + + if "multipleOf" in properties: + _field_properties["multiple_of"] = properties["multipleOf"] + + return int, Field(**_field_properties) diff --git a/pyproject.toml b/pyproject.toml index 33a55b6..32fc4ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dev = [ # POE Tasks [tool.poe.tasks] create-hooks = "bash .githooks/set-hooks.sh" - +tests = "python -m unittest discover -s tests -v" # Build System [tool.hatch.version] diff --git a/tests/test_type_parser.py b/tests/test_type_parser.py index b3a1886..c96c629 100644 --- a/tests/test_type_parser.py +++ b/tests/test_type_parser.py @@ -21,9 +21,10 @@ class TestTypeParser(unittest.TestCase): def test_int_parser(self): parser = IntTypeParser() - expected_definition = (int, {}) - self.assertEqual(parser.from_properties("placeholder", {}), expected_definition) + type_parsing, type_validator = parser.from_properties("placeholder", {}) + + self.assertEqual(type_parsing, int) def test_float_parser(self): parser = FloatTypeParser() -- 2.49.1 From 9e1763c35af055b0a0f1b4546e5495cc27420bb7 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Tue, 8 Apr 2025 00:18:24 -0300 Subject: [PATCH 2/8] Implements Int, Float, String Validators --- jambo/{types => parser}/__init__.py | 0 jambo/{types => parser}/_type_parser.py | 0 jambo/{types => parser}/array_type_parser.py | 2 +- .../{types => parser}/boolean_type_parser.py | 2 +- jambo/parser/float_type_parser.py | 12 +++++ jambo/parser/int_type_parser.py | 12 +++++ jambo/{types => parser}/object_type_parser.py | 2 +- jambo/parser/string_type_parser.py | 21 ++++++++ jambo/schema_converter.py | 4 +- jambo/types/float_type_parser.py | 11 ---- jambo/types/int_type_parser.py | 24 --------- jambo/types/string_type_parser.py | 11 ---- jambo/utils/__init__.py | 0 jambo/utils/properties_builder/__init__.py | 0 .../numeric_properties_builder.py | 14 +++++ jambo/utils/types/__init__.py | 0 tests/test_type_parser.py | 52 ++++++++++++++++--- 17 files changed, 110 insertions(+), 57 deletions(-) rename jambo/{types => parser}/__init__.py (100%) rename jambo/{types => parser}/_type_parser.py (100%) rename jambo/{types => parser}/array_type_parser.py (87%) rename jambo/{types => parser}/boolean_type_parser.py (77%) create mode 100644 jambo/parser/float_type_parser.py create mode 100644 jambo/parser/int_type_parser.py rename jambo/{types => parser}/object_type_parser.py (84%) create mode 100644 jambo/parser/string_type_parser.py delete mode 100644 jambo/types/float_type_parser.py delete mode 100644 jambo/types/int_type_parser.py delete mode 100644 jambo/types/string_type_parser.py create mode 100644 jambo/utils/__init__.py create mode 100644 jambo/utils/properties_builder/__init__.py create mode 100644 jambo/utils/properties_builder/numeric_properties_builder.py create mode 100644 jambo/utils/types/__init__.py diff --git a/jambo/types/__init__.py b/jambo/parser/__init__.py similarity index 100% rename from jambo/types/__init__.py rename to jambo/parser/__init__.py diff --git a/jambo/types/_type_parser.py b/jambo/parser/_type_parser.py similarity index 100% rename from jambo/types/_type_parser.py rename to jambo/parser/_type_parser.py diff --git a/jambo/types/array_type_parser.py b/jambo/parser/array_type_parser.py similarity index 87% rename from jambo/types/array_type_parser.py rename to jambo/parser/array_type_parser.py index 328a6d4..52fec64 100644 --- a/jambo/types/array_type_parser.py +++ b/jambo/parser/array_type_parser.py @@ -1,4 +1,4 @@ -from jambo.types._type_parser import GenericTypeParser +from jambo.parser._type_parser import GenericTypeParser from typing import TypeVar diff --git a/jambo/types/boolean_type_parser.py b/jambo/parser/boolean_type_parser.py similarity index 77% rename from jambo/types/boolean_type_parser.py rename to jambo/parser/boolean_type_parser.py index 2af98fb..198154f 100644 --- a/jambo/types/boolean_type_parser.py +++ b/jambo/parser/boolean_type_parser.py @@ -1,4 +1,4 @@ -from jambo.types._type_parser import GenericTypeParser +from jambo.parser._type_parser import GenericTypeParser class BooleanTypeParser(GenericTypeParser): diff --git a/jambo/parser/float_type_parser.py b/jambo/parser/float_type_parser.py new file mode 100644 index 0000000..c303ff7 --- /dev/null +++ b/jambo/parser/float_type_parser.py @@ -0,0 +1,12 @@ +from jambo.parser._type_parser import GenericTypeParser +from jambo.utils.properties_builder.numeric_properties_builder import numeric_properties_builder + + +class FloatTypeParser(GenericTypeParser): + mapped_type = float + + json_schema_type = "number" + + @staticmethod + def from_properties(name, properties): + return float, numeric_properties_builder(properties) diff --git a/jambo/parser/int_type_parser.py b/jambo/parser/int_type_parser.py new file mode 100644 index 0000000..365e346 --- /dev/null +++ b/jambo/parser/int_type_parser.py @@ -0,0 +1,12 @@ +from jambo.parser._type_parser import GenericTypeParser +from jambo.utils.properties_builder.numeric_properties_builder import numeric_properties_builder + + +class IntTypeParser(GenericTypeParser): + mapped_type = int + + json_schema_type = "integer" + + @staticmethod + def from_properties(name, properties): + return int, numeric_properties_builder(properties) diff --git a/jambo/types/object_type_parser.py b/jambo/parser/object_type_parser.py similarity index 84% rename from jambo/types/object_type_parser.py rename to jambo/parser/object_type_parser.py index b63de81..6fd0c16 100644 --- a/jambo/types/object_type_parser.py +++ b/jambo/parser/object_type_parser.py @@ -1,4 +1,4 @@ -from jambo.types._type_parser import GenericTypeParser +from jambo.parser._type_parser import GenericTypeParser class ObjectTypeParser(GenericTypeParser): diff --git a/jambo/parser/string_type_parser.py b/jambo/parser/string_type_parser.py new file mode 100644 index 0000000..ee088de --- /dev/null +++ b/jambo/parser/string_type_parser.py @@ -0,0 +1,21 @@ +from jambo.parser._type_parser import GenericTypeParser + + +class StringTypeParser(GenericTypeParser): + mapped_type = str + + json_schema_type = "string" + + @staticmethod + def from_properties(name, properties): + _mappings = { + "maxLength": "max_length", + "minLength": "min_length", + "pattern": "pattern", + } + + return str, { + _mappings[key]: value + for key, value in properties.items() + if key in _mappings + } diff --git a/jambo/schema_converter.py b/jambo/schema_converter.py index e69d1b8..196fc8f 100644 --- a/jambo/schema_converter.py +++ b/jambo/schema_converter.py @@ -1,4 +1,4 @@ -from jambo.types import GenericTypeParser +from jambo.parser import GenericTypeParser from jsonschema.exceptions import SchemaError from jsonschema.protocols import Validator @@ -78,7 +78,7 @@ class SchemaConverter: @staticmethod def _build_field( name, properties: dict, required_keys: list[str] - ) -> tuple[type, Field]: + ) -> tuple[type, dict]: _field_type, _field_args = GenericTypeParser.get_impl( properties["type"] ).from_properties(name, properties) diff --git a/jambo/types/float_type_parser.py b/jambo/types/float_type_parser.py deleted file mode 100644 index f10f3e3..0000000 --- a/jambo/types/float_type_parser.py +++ /dev/null @@ -1,11 +0,0 @@ -from jambo.types._type_parser import GenericTypeParser - - -class FloatTypeParser(GenericTypeParser): - mapped_type = float - - json_schema_type = "number" - - @staticmethod - def from_properties(name, properties): - return float, {} diff --git a/jambo/types/int_type_parser.py b/jambo/types/int_type_parser.py deleted file mode 100644 index 4fa9778..0000000 --- a/jambo/types/int_type_parser.py +++ /dev/null @@ -1,24 +0,0 @@ -from dataclasses import Field - -from jambo.types._type_parser import GenericTypeParser - - -class IntTypeParser(GenericTypeParser): - mapped_type = int - - json_schema_type = "integer" - - @staticmethod - def from_properties(name, properties): - _field_properties = dict() - - if "minimum" in properties: - _field_properties["ge"] = properties["minimum"] - - if "maximum" in properties: - _field_properties["le"] = properties["maximum"] - - if "multipleOf" in properties: - _field_properties["multiple_of"] = properties["multipleOf"] - - return int, Field(**_field_properties) diff --git a/jambo/types/string_type_parser.py b/jambo/types/string_type_parser.py deleted file mode 100644 index d846eed..0000000 --- a/jambo/types/string_type_parser.py +++ /dev/null @@ -1,11 +0,0 @@ -from jambo.types._type_parser import GenericTypeParser - - -class StringTypeParser(GenericTypeParser): - mapped_type = str - - json_schema_type = "string" - - @staticmethod - def from_properties(name, properties): - return str, {} diff --git a/jambo/utils/__init__.py b/jambo/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jambo/utils/properties_builder/__init__.py b/jambo/utils/properties_builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jambo/utils/properties_builder/numeric_properties_builder.py b/jambo/utils/properties_builder/numeric_properties_builder.py new file mode 100644 index 0000000..fd5b0cf --- /dev/null +++ b/jambo/utils/properties_builder/numeric_properties_builder.py @@ -0,0 +1,14 @@ +def numeric_properties_builder(properties): + _mappings = { + "minimum": "ge", + "exclusiveMinimum": "gt", + "maximum": "le", + "exclusiveMaximum": "lt", + "multipleOf": "multiple_of", + } + + return { + _mappings[key]: value + for key, value in properties.items() + if key in _mappings + } \ No newline at end of file diff --git a/jambo/utils/types/__init__.py b/jambo/utils/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_type_parser.py b/tests/test_type_parser.py index c96c629..1ac8d3e 100644 --- a/tests/test_type_parser.py +++ b/tests/test_type_parser.py @@ -1,4 +1,4 @@ -from jambo.types import ( +from jambo.parser import ( ArrayTypeParser, FloatTypeParser, GenericTypeParser, @@ -22,21 +22,61 @@ class TestTypeParser(unittest.TestCase): def test_int_parser(self): parser = IntTypeParser() - type_parsing, type_validator = parser.from_properties("placeholder", {}) + type_parsing, type_validator = parser.from_properties( + "placeholder", { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 1, + "maximum": 10, + "exclusiveMaximum": 11, + "multipleOf": 2, + } + ) self.assertEqual(type_parsing, int) + self.assertEqual(type_validator["ge"], 0) + self.assertEqual(type_validator["gt"], 1) + self.assertEqual(type_validator["le"], 10) + self.assertEqual(type_validator["lt"], 11) + self.assertEqual(type_validator["multiple_of"], 2) def test_float_parser(self): parser = FloatTypeParser() - expected_definition = (float, {}) - self.assertEqual(parser.from_properties("placeholder", {}), expected_definition) + type_parsing, type_validator = parser.from_properties( + "placeholder", { + "type": "number", + "minimum": 0, + "exclusiveMinimum": 1, + "maximum": 10, + "exclusiveMaximum": 11, + "multipleOf": 2, + } + ) + + self.assertEqual(type_parsing, float) + self.assertEqual(type_validator["ge"], 0) + self.assertEqual(type_validator["gt"], 1) + self.assertEqual(type_validator["le"], 10) + self.assertEqual(type_validator["lt"], 11) + self.assertEqual(type_validator["multiple_of"], 2) def test_string_parser(self): parser = StringTypeParser() - expected_definition = (str, {}) - self.assertEqual(parser.from_properties("placeholder", {}), expected_definition) + type_parsing, type_validator = parser.from_properties( + "placeholder", { + "type": "string", + "maxLength": 10, + "minLength": 1, + "pattern": "[a-zA-Z0-9]", + } + ) + + self.assertEqual(type_parsing, str) + self.assertEqual(type_validator["max_length"], 10) + self.assertEqual(type_validator["min_length"], 1) + self.assertEqual(type_validator["pattern"], "[a-zA-Z0-9]") def test_object_parser(self): parser = ObjectTypeParser() -- 2.49.1 From 63dc0de4b28ae1a5771603b96240d86c4a6d4f20 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Wed, 9 Apr 2025 21:20:46 -0300 Subject: [PATCH 3/8] Final Implementation of Validation Options --- jambo/parser/array_type_parser.py | 12 +++++++- jambo/parser/boolean_type_parser.py | 2 +- jambo/parser/object_type_parser.py | 6 ++-- jambo/parser/string_type_parser.py | 9 +++--- .../mappings_properties_builder.py | 4 +++ .../numeric_properties_builder.py | 11 +++---- tests/test_type_parser.py | 29 +++++++++++++------ 7 files changed, 50 insertions(+), 23 deletions(-) create mode 100644 jambo/utils/properties_builder/mappings_properties_builder.py diff --git a/jambo/parser/array_type_parser.py b/jambo/parser/array_type_parser.py index 52fec64..f0329ac 100644 --- a/jambo/parser/array_type_parser.py +++ b/jambo/parser/array_type_parser.py @@ -2,6 +2,10 @@ from jambo.parser._type_parser import GenericTypeParser from typing import TypeVar +from jambo.utils.properties_builder.mappings_properties_builder import ( + mappings_properties_builder, +) + V = TypeVar("V") @@ -16,4 +20,10 @@ class ArrayTypeParser(GenericTypeParser): properties["items"]["type"] ).from_properties(name, properties["items"]) - return list[_item_type], {} + _mappings = { + "maxItems": "max_items", + "minItems": "min_items", + "uniqueItems": "unique_items", + } + + return list[_item_type], mappings_properties_builder(properties, _mappings) diff --git a/jambo/parser/boolean_type_parser.py b/jambo/parser/boolean_type_parser.py index 198154f..c549062 100644 --- a/jambo/parser/boolean_type_parser.py +++ b/jambo/parser/boolean_type_parser.py @@ -8,4 +8,4 @@ class BooleanTypeParser(GenericTypeParser): @staticmethod def from_properties(name, properties): - return bool, {} + return bool, {} # The second argument is not used in this case diff --git a/jambo/parser/object_type_parser.py b/jambo/parser/object_type_parser.py index 6fd0c16..b5e7f5d 100644 --- a/jambo/parser/object_type_parser.py +++ b/jambo/parser/object_type_parser.py @@ -10,5 +10,7 @@ class ObjectTypeParser(GenericTypeParser): def from_properties(name, properties): from jambo.schema_converter import SchemaConverter - _type = SchemaConverter.build_object(name, properties) - return _type, {} + return ( + SchemaConverter.build_object(name, properties), + {}, # The second argument is not used in this case + ) diff --git a/jambo/parser/string_type_parser.py b/jambo/parser/string_type_parser.py index ee088de..c6ac1e4 100644 --- a/jambo/parser/string_type_parser.py +++ b/jambo/parser/string_type_parser.py @@ -1,4 +1,7 @@ from jambo.parser._type_parser import GenericTypeParser +from jambo.utils.properties_builder.mappings_properties_builder import ( + mappings_properties_builder, +) class StringTypeParser(GenericTypeParser): @@ -14,8 +17,4 @@ class StringTypeParser(GenericTypeParser): "pattern": "pattern", } - return str, { - _mappings[key]: value - for key, value in properties.items() - if key in _mappings - } + return str, mappings_properties_builder(properties, _mappings) diff --git a/jambo/utils/properties_builder/mappings_properties_builder.py b/jambo/utils/properties_builder/mappings_properties_builder.py new file mode 100644 index 0000000..64a7a72 --- /dev/null +++ b/jambo/utils/properties_builder/mappings_properties_builder.py @@ -0,0 +1,4 @@ +def mappings_properties_builder(properties, mappings): + return { + mappings[key]: value for key, value in properties.items() if key in mappings + } diff --git a/jambo/utils/properties_builder/numeric_properties_builder.py b/jambo/utils/properties_builder/numeric_properties_builder.py index fd5b0cf..ff512db 100644 --- a/jambo/utils/properties_builder/numeric_properties_builder.py +++ b/jambo/utils/properties_builder/numeric_properties_builder.py @@ -1,3 +1,8 @@ +from jambo.utils.properties_builder.mappings_properties_builder import ( + mappings_properties_builder, +) + + def numeric_properties_builder(properties): _mappings = { "minimum": "ge", @@ -7,8 +12,4 @@ def numeric_properties_builder(properties): "multipleOf": "multiple_of", } - return { - _mappings[key]: value - for key, value in properties.items() - if key in _mappings - } \ No newline at end of file + return mappings_properties_builder(properties, _mappings) diff --git a/tests/test_type_parser.py b/tests/test_type_parser.py index 1ac8d3e..54e0ea5 100644 --- a/tests/test_type_parser.py +++ b/tests/test_type_parser.py @@ -23,14 +23,15 @@ class TestTypeParser(unittest.TestCase): parser = IntTypeParser() type_parsing, type_validator = parser.from_properties( - "placeholder", { + "placeholder", + { "type": "integer", "minimum": 0, "exclusiveMinimum": 1, "maximum": 10, "exclusiveMaximum": 11, "multipleOf": 2, - } + }, ) self.assertEqual(type_parsing, int) @@ -44,14 +45,15 @@ class TestTypeParser(unittest.TestCase): parser = FloatTypeParser() type_parsing, type_validator = parser.from_properties( - "placeholder", { + "placeholder", + { "type": "number", "minimum": 0, "exclusiveMinimum": 1, "maximum": 10, "exclusiveMaximum": 11, "multipleOf": 2, - } + }, ) self.assertEqual(type_parsing, float) @@ -65,12 +67,13 @@ class TestTypeParser(unittest.TestCase): parser = StringTypeParser() type_parsing, type_validator = parser.from_properties( - "placeholder", { + "placeholder", + { "type": "string", "maxLength": 10, "minLength": 1, "pattern": "[a-zA-Z0-9]", - } + }, ) self.assertEqual(type_parsing, str) @@ -110,19 +113,27 @@ class TestTypeParser(unittest.TestCase): parser = ArrayTypeParser() properties = { + "type": "array", "items": { "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer"}, }, - } + }, + "maxItems": 10, + "minItems": 1, + "uniqueItems": True, } - _type, _args = parser.from_properties("placeholder", properties) + type_parsing, type_validator = parser.from_properties("placeholder", properties) - Model = get_args(_type)[0] + Model = get_args(type_parsing)[0] obj = Model(name="name", age=10) self.assertEqual(obj.name, "name") self.assertEqual(obj.age, 10) + + self.assertEqual(type_validator["max_items"], 10) + self.assertEqual(type_validator["min_items"], 1) + self.assertEqual(type_validator["unique_items"], True) -- 2.49.1 From 6ea5d204aeda5f08d0467b0b748ad9d28eb9aec0 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Wed, 9 Apr 2025 21:37:58 -0300 Subject: [PATCH 4/8] Final Working Pydantics Validations --- jambo/parser/array_type_parser.py | 11 ++++-- tests/test_schema_converter.py | 62 +++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/jambo/parser/array_type_parser.py b/jambo/parser/array_type_parser.py index f0329ac..e7f6a6e 100644 --- a/jambo/parser/array_type_parser.py +++ b/jambo/parser/array_type_parser.py @@ -21,9 +21,12 @@ class ArrayTypeParser(GenericTypeParser): ).from_properties(name, properties["items"]) _mappings = { - "maxItems": "max_items", - "minItems": "min_items", - "uniqueItems": "unique_items", + "maxItems": "max_length", + "minItems": "min_length", } - return list[_item_type], mappings_properties_builder(properties, _mappings) + wrapper_type = set if properties.get("uniqueItems", False) else list + + return wrapper_type[_item_type], mappings_properties_builder( + properties, _mappings + ) diff --git a/tests/test_schema_converter.py b/tests/test_schema_converter.py index d6403f8..c24d160 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -32,7 +32,13 @@ class TestSchemaConverter(TestCase): "description": "A person", "type": "object", "properties": { - "name": {"type": "string"}, + "name": {"type": "string", "maxLength": 4, "minLength": 1}, + "email": { + "type": "string", + "maxLength": 50, + "minLength": 5, + "pattern": r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", + }, }, "required": ["name"], } @@ -41,13 +47,29 @@ class TestSchemaConverter(TestCase): self.assertEqual(model(name="John", age=30).name, "John") + with self.assertRaises(ValueError): + model(name=123, age=30, email="teste@hideyoshi.com") + + with self.assertRaises(ValueError): + model(name="John Invalid", age=45, email="teste@hideyoshi.com") + + with self.assertRaises(ValueError): + model(name="", age=45, email="teste@hideyoshi.com") + + with self.assertRaises(ValueError): + model(name="John", age=45, email="hideyoshi.com") + def test_validation_integer(self): schema = { "title": "Person", "description": "A person", "type": "object", "properties": { - "age": {"type": "integer"}, + "age": { + "type": "integer", + "minimum": 0, + "maximum": 120, + }, }, "required": ["age"], } @@ -56,7 +78,11 @@ class TestSchemaConverter(TestCase): self.assertEqual(model(age=30).age, 30) - self.assertEqual(model(age="30").age, 30) + with self.assertRaises(ValueError): + model(age=-1) + + with self.assertRaises(ValueError): + model(age=121) def test_validation_float(self): schema = { @@ -64,7 +90,11 @@ class TestSchemaConverter(TestCase): "description": "A person", "type": "object", "properties": { - "age": {"type": "number"}, + "age": { + "type": "number", + "minimum": 0, + "maximum": 120, + }, }, "required": ["age"], } @@ -73,7 +103,11 @@ class TestSchemaConverter(TestCase): self.assertEqual(model(age=30).age, 30.0) - self.assertEqual(model(age="30").age, 30.0) + with self.assertRaises(ValueError): + model(age=-1.0) + + with self.assertRaises(ValueError): + model(age=121.0) def test_validation_boolean(self): schema = { @@ -98,14 +132,28 @@ class TestSchemaConverter(TestCase): "description": "A person", "type": "object", "properties": { - "friends": {"type": "array", "items": {"type": "string"}}, + "friends": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1, + "maxItems": 2, + "uniqueItems": True, + }, }, "required": ["friends"], } model = SchemaConverter.build(schema) - self.assertEqual(model(friends=["John", "Jane"]).friends, ["John", "Jane"]) + self.assertEqual( + model(friends=["John", "Jane", "John"]).friends, {"John", "Jane"} + ) + + with self.assertRaises(ValueError): + model(friends=[]) + + with self.assertRaises(ValueError): + model(friends=["John", "Jane", "Invalid"]) def test_validation_object(self): schema = { -- 2.49.1 From 373c79d5f1a71ede6ff75041e5101c9950f7dea7 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Wed, 9 Apr 2025 22:14:34 -0300 Subject: [PATCH 5/8] Adds TypedDict for JsonSchema for Reference --- jambo/schema_converter.py | 6 ++- jambo/types/json_schema_type.py | 80 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 jambo/types/json_schema_type.py diff --git a/jambo/schema_converter.py b/jambo/schema_converter.py index 196fc8f..676a78b 100644 --- a/jambo/schema_converter.py +++ b/jambo/schema_converter.py @@ -7,6 +7,8 @@ from pydantic.fields import Field from typing import Type +from jambo.types.json_schema_type import JSONSchema + class SchemaConverter: """ @@ -18,7 +20,7 @@ class SchemaConverter: """ @staticmethod - def build(schema: dict) -> Type: + def build(schema: JSONSchema) -> Type: """ Converts a JSON Schema to a Pydantic model. :param schema: The JSON Schema to convert. @@ -32,7 +34,7 @@ class SchemaConverter: @staticmethod def build_object( name: str, - schema: dict, + schema: JSONSchema, ) -> Type: """ Converts a JSON Schema object to a Pydantic model given a name. diff --git a/jambo/types/json_schema_type.py b/jambo/types/json_schema_type.py new file mode 100644 index 0000000..77ec7ff --- /dev/null +++ b/jambo/types/json_schema_type.py @@ -0,0 +1,80 @@ +from typing import List, Dict, Union, TypedDict, Literal + + +JSONSchemaType = Literal[ + "string", "number", "integer", "boolean", "object", "array", "null" +] + + +JSONType = Union[str, int, float, bool, None, Dict[str, "JSONType"], List["JSONType"]] + + +class JSONSchema(TypedDict, total=False): + # Basic metadata + title: str + description: str + default: JSONType + examples: List[JSONType] + + # Type definitions + type: Union[JSONSchemaType, List[JSONSchemaType]] + + # Object-specific keywords + properties: Dict[str, "JSONSchema"] + required: List[str] + additionalProperties: Union[bool, "JSONSchema"] + minProperties: int + maxProperties: int + patternProperties: Dict[str, "JSONSchema"] + dependencies: Dict[str, Union[List[str], "JSONSchema"]] + + # Array-specific keywords + items: Union["JSONSchema", List["JSONSchema"]] + additionalItems: Union[bool, "JSONSchema"] + minItems: int + maxItems: int + uniqueItems: bool + + # String-specific keywords + minLength: int + maxLength: int + pattern: str + format: str + + # Number-specific keywords + minimum: float + maximum: float + exclusiveMinimum: float + exclusiveMaximum: float + multipleOf: float + + # Enum and const + enum: List[JSONType] + const: JSONType + + # Conditionals + if_: "JSONSchema" # 'if' is a reserved word in Python + then: "JSONSchema" + else_: "JSONSchema" # 'else' is also a reserved word + + # Combination keywords + allOf: List["JSONSchema"] + anyOf: List["JSONSchema"] + oneOf: List["JSONSchema"] + not_: "JSONSchema" # 'not' is a reserved word + + +# Fix forward references +JSONSchema.__annotations__["properties"] = Dict[str, JSONSchema] +JSONSchema.__annotations__["items"] = Union[JSONSchema, List[JSONSchema]] +JSONSchema.__annotations__["additionalItems"] = Union[bool, JSONSchema] +JSONSchema.__annotations__["additionalProperties"] = Union[bool, JSONSchema] +JSONSchema.__annotations__["patternProperties"] = Dict[str, JSONSchema] +JSONSchema.__annotations__["dependencies"] = Dict[str, Union[List[str], JSONSchema]] +JSONSchema.__annotations__["if_"] = JSONSchema +JSONSchema.__annotations__["then"] = JSONSchema +JSONSchema.__annotations__["else_"] = JSONSchema +JSONSchema.__annotations__["allOf"] = List[JSONSchema] +JSONSchema.__annotations__["anyOf"] = List[JSONSchema] +JSONSchema.__annotations__["oneOf"] = List[JSONSchema] +JSONSchema.__annotations__["not_"] = JSONSchema -- 2.49.1 From 00721c936a0cfd9ccc874d80064dc09aed4f644d Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Wed, 9 Apr 2025 22:41:27 -0300 Subject: [PATCH 6/8] Fixes Tests for Array Parser --- tests/test_type_parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_type_parser.py b/tests/test_type_parser.py index 54e0ea5..cee9818 100644 --- a/tests/test_type_parser.py +++ b/tests/test_type_parser.py @@ -128,12 +128,12 @@ class TestTypeParser(unittest.TestCase): type_parsing, type_validator = parser.from_properties("placeholder", properties) + self.assertEqual(type_parsing.__origin__, set) + self.assertEqual(type_validator["max_length"], 10) + self.assertEqual(type_validator["min_length"], 1) + Model = get_args(type_parsing)[0] obj = Model(name="name", age=10) self.assertEqual(obj.name, "name") self.assertEqual(obj.age, 10) - - self.assertEqual(type_validator["max_items"], 10) - self.assertEqual(type_validator["min_items"], 1) - self.assertEqual(type_validator["unique_items"], True) -- 2.49.1 From 1e59268cd3f50e5992ebbe7fb8296f68e07dd775 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Wed, 9 Apr 2025 23:08:51 -0300 Subject: [PATCH 7/8] Adds Build and Test Pipeline --- .github/workflows/build.yml | 66 +++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4263e09 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,66 @@ +name: Example + + +on: + push + +permissions: + contents: read + +jobs: + test: + name: run-tests + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + # Install a specific version of uv. + version: "0.6.14" + enable-cache: true + cache-dependency-glob: "uv.lock" + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: Run tests + run: uv run poe tests + + publish: + name: publish + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + needs: [test] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + # Install a specific version of uv. + version: "0.6.14" + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python + run: uv python install + + - name: Build + run: uv build + + - name: Publish + run: uv publish -t ${{ secrets.PYPI_TOKEN }} \ No newline at end of file -- 2.49.1 From eb9427c72e3aa1e446702943a12e008791b7b1f9 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Wed, 9 Apr 2025 23:19:29 -0300 Subject: [PATCH 8/8] Fixes Support for Python3.10 --- jambo/parser/_type_parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jambo/parser/_type_parser.py b/jambo/parser/_type_parser.py index a2ec13f..16dd833 100644 --- a/jambo/parser/_type_parser.py +++ b/jambo/parser/_type_parser.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod -from typing import Generic, Self, TypeVar +from typing import Generic, TypeVar +from typing_extensions import Self from pydantic import Field -- 2.49.1