From dd31f62ef2fca788221f55c456e9499e2deb9159 Mon Sep 17 00:00:00 2001 From: JCHacking Date: Mon, 1 Dec 2025 17:38:31 +0100 Subject: [PATCH 1/2] feat: duration string parser --- .githooks/pre-commit-config.yaml | 2 +- jambo/parser/string_type_parser.py | 29 +++---------------------- jambo/schema_converter.py | 6 ++--- tests/parser/test_string_type_parser.py | 8 +++---- tests/test_schema_converter.py | 18 ++++++++------- 5 files changed, 19 insertions(+), 44 deletions(-) diff --git a/.githooks/pre-commit-config.yaml b/.githooks/pre-commit-config.yaml index 08560b3..9aa9bb0 100644 --- a/.githooks/pre-commit-config.yaml +++ b/.githooks/pre-commit-config.yaml @@ -1,7 +1,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.11.4 + rev: v0.14.7 hooks: # Run the linter. - id: ruff diff --git a/jambo/parser/string_type_parser.py b/jambo/parser/string_type_parser.py index 4c18761..97f3886 100644 --- a/jambo/parser/string_type_parser.py +++ b/jambo/parser/string_type_parser.py @@ -2,8 +2,8 @@ from jambo.exceptions import InvalidSchemaException from jambo.parser._type_parser import GenericTypeParser from jambo.types.type_parser_options import TypeParserOptions -from pydantic import AnyUrl, EmailStr -from typing_extensions import Any, Unpack +from pydantic import AnyUrl, EmailStr, TypeAdapter +from typing_extensions import Unpack from datetime import date, datetime, time, timedelta from ipaddress import IPv4Address, IPv6Address @@ -64,7 +64,7 @@ class StringTypeParser(GenericTypeParser): if "examples" in mapped_properties: mapped_properties["examples"] = [ - self._parse_example(example, format_type, mapped_type) + TypeAdapter(mapped_type).validate_python(example) for example in mapped_properties["examples"] ] @@ -73,26 +73,3 @@ class StringTypeParser(GenericTypeParser): mapped_properties["json_schema_extra"]["format"] = format_type return mapped_type, mapped_properties - - def _parse_example( - self, example: Any, format_type: str, mapped_type: type[Any] - ) -> Any: - """ - Parse example from JSON Schema format to python format - :param example: Example Value - :param format_type: Format Type - :param mapped_type: Type to parse - :return: Example parsed - """ - match format_type: - case "date" | "time" | "date-time": - return mapped_type.fromisoformat(example) - case "duration": - # TODO: Implement duration parser - raise NotImplementedError - case "ipv4" | "ipv6": - return mapped_type(example) - case "uuid": - return mapped_type(example) - case _: - return example diff --git a/jambo/schema_converter.py b/jambo/schema_converter.py index 0e92119..2e46828 100644 --- a/jambo/schema_converter.py +++ b/jambo/schema_converter.py @@ -128,10 +128,8 @@ class SchemaConverter: Gets a cached reference from the reference cache. :param ref_name: The name of the reference to get. :return: The cached reference, or None if not found. - """ - cached_type = self._namespace_registry.get( - namespace, {} - ).get(ref_name) + """ + cached_type = self._namespace_registry.get(namespace, {}).get(ref_name) if isinstance(cached_type, type): return cached_type diff --git a/tests/parser/test_string_type_parser.py b/tests/parser/test_string_type_parser.py index b00b487..56334ec 100644 --- a/tests/parser/test_string_type_parser.py +++ b/tests/parser/test_string_type_parser.py @@ -3,7 +3,6 @@ from jambo.parser import StringTypeParser from pydantic import AnyUrl, EmailStr -import unittest from datetime import date, datetime, time, timedelta, timezone from ipaddress import IPv4Address, IPv6Address, ip_address from unittest import TestCase @@ -121,7 +120,7 @@ class TestStringTypeParser(TestCase): type_parsing, type_validator = parser.from_properties("placeholder", properties) self.assertEqual(type_parsing, AnyUrl) - self.assertEqual(type_validator["examples"], ["test://domain/resource"]) + self.assertEqual(type_validator["examples"], [AnyUrl("test://domain/resource")]) def test_string_parser_with_ip_formats(self): parser = StringTypeParser() @@ -299,7 +298,6 @@ class TestStringTypeParser(TestCase): }, ) - @unittest.skip("Duration parsing not yet implemented") def test_string_parser_with_timedelta_format(self): parser = StringTypeParser() @@ -315,9 +313,9 @@ class TestStringTypeParser(TestCase): self.assertEqual( type_validator["examples"], [ - timedelta(days=7), + timedelta(days=428, hours=4, minutes=5, seconds=6), timedelta(minutes=30), - timedelta(hours=4, minutes=5, seconds=6), + timedelta(days=7), timedelta(seconds=0.5), ], ) diff --git a/tests/test_schema_converter.py b/tests/test_schema_converter.py index dd2b32c..e9bcb5b 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -1109,7 +1109,7 @@ class TestSchemaConverter(TestCase): def test_namespace_isolation_via_on_call_config(self): namespace = "namespace1" - + schema: JSONSchema = { "$id": namespace, "title": "Person", @@ -1130,16 +1130,16 @@ class TestSchemaConverter(TestCase): } model = self.converter.build_with_cache(schema) - + invalid_cached_model = self.converter.get_cached_ref("Person") self.assertIsNone(invalid_cached_model) - + cached_model = self.converter.get_cached_ref("Person", namespace=namespace) self.assertIs(model, cached_model) def test_clear_namespace_registry(self): namespace = "namespace_to_clear" - + schema: JSONSchema = { "$id": namespace, "title": "Person", @@ -1160,11 +1160,13 @@ class TestSchemaConverter(TestCase): } model = self.converter.build_with_cache(schema) - + cached_model = self.converter.get_cached_ref("Person", namespace=namespace) self.assertIs(model, cached_model) - + self.converter.clear_ref_cache(namespace=namespace) - - cleared_cached_model = self.converter.get_cached_ref("Person", namespace=namespace) + + cleared_cached_model = self.converter.get_cached_ref( + "Person", namespace=namespace + ) self.assertIsNone(cleared_cached_model) -- 2.49.1 From 422cc2efe00c32c55d4d7a79e4a68d0af30f70b2 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Fri, 5 Dec 2025 20:15:08 -0300 Subject: [PATCH 2/2] feat: captures any str validation exception and converts it into ValidationError converts any exception thrown in the str parser example validation into ValidationError so that the user knows that this is a error in the schema and not a parsing validation exception --- jambo/parser/string_type_parser.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/jambo/parser/string_type_parser.py b/jambo/parser/string_type_parser.py index 97f3886..b09cf20 100644 --- a/jambo/parser/string_type_parser.py +++ b/jambo/parser/string_type_parser.py @@ -2,7 +2,7 @@ from jambo.exceptions import InvalidSchemaException from jambo.parser._type_parser import GenericTypeParser from jambo.types.type_parser_options import TypeParserOptions -from pydantic import AnyUrl, EmailStr, TypeAdapter +from pydantic import AnyUrl, EmailStr, TypeAdapter, ValidationError from typing_extensions import Unpack from datetime import date, datetime, time, timedelta @@ -62,11 +62,16 @@ class StringTypeParser(GenericTypeParser): if format_type in self.format_pattern_mapping: mapped_properties["pattern"] = self.format_pattern_mapping[format_type] - if "examples" in mapped_properties: - mapped_properties["examples"] = [ - TypeAdapter(mapped_type).validate_python(example) - for example in mapped_properties["examples"] - ] + try: + if "examples" in mapped_properties: + mapped_properties["examples"] = [ + TypeAdapter(mapped_type).validate_python(example) + for example in mapped_properties["examples"] + ] + except ValidationError as err: + raise InvalidSchemaException( + f"Invalid example type for field {name}." + ) from err if "json_schema_extra" not in mapped_properties: mapped_properties["json_schema_extra"] = {} -- 2.49.1