10 Commits

Author SHA1 Message Date
6440c30a91 Merge pull request #76 from HideyoshiNakazone/chore/adds-python3.14-metadata
chore: adds python3.14 metadata
2025-12-08 19:07:19 -03:00
19d1f72951 fix: minor fix in internal typing 2025-12-08 19:04:17 -03:00
02a28c9586 chore: updates uv version in ci 2025-12-08 18:56:55 -03:00
eee32a02ae chore: adds python3.14 to metadata 2025-12-08 18:56:01 -03:00
00802744dd Merge pull request #74 from HideyoshiNakazone/chore/run-tests-on-python3.14
chore: updates project to be python3.14 compatible
2025-12-06 17:09:21 -03:00
dd2e7d221c chore: updates uv version in github action file 2025-12-06 17:05:42 -03:00
558abf5d40 chore: updates project to be python3.14 compatible 2025-12-06 17:01:24 -03:00
70afa80ccf Merge pull request #73 from JCHacking/string-duration
feat: duration string parser
2025-12-06 16:53:27 -03:00
422cc2efe0 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
2025-12-05 20:15:08 -03:00
JCHacking
dd31f62ef2 feat: duration string parser 2025-12-01 17:38:31 +01:00
9 changed files with 769 additions and 574 deletions

View File

@@ -1,7 +1,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.11.4 rev: v0.14.7
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -23,6 +23,7 @@ jobs:
- "3.11" - "3.11"
- "3.12" - "3.12"
- "3.13" - "3.13"
- "3.14"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -31,7 +32,7 @@ jobs:
uses: astral-sh/setup-uv@v5 uses: astral-sh/setup-uv@v5
with: with:
# Install a specific version of uv. # Install a specific version of uv.
version: "0.6.14" version: "0.9.15"
enable-cache: true enable-cache: true
cache-dependency-glob: "uv.lock" cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@@ -68,7 +69,7 @@ jobs:
uses: astral-sh/setup-uv@v5 uses: astral-sh/setup-uv@v5
with: with:
# Install a specific version of uv. # Install a specific version of uv.
version: "0.6.14" version: "0.9.15"
enable-cache: true enable-cache: true
cache-dependency-glob: "uv.lock" cache-dependency-glob: "uv.lock"

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.14

View File

@@ -2,8 +2,8 @@ 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 AnyUrl, EmailStr from pydantic import AnyUrl, EmailStr, TypeAdapter, ValidationError
from typing_extensions import Any, Unpack from typing_extensions import Unpack
from datetime import date, datetime, time, timedelta from datetime import date, datetime, time, timedelta
from ipaddress import IPv4Address, IPv6Address from ipaddress import IPv4Address, IPv6Address
@@ -62,37 +62,19 @@ class StringTypeParser(GenericTypeParser):
if format_type in self.format_pattern_mapping: if format_type in self.format_pattern_mapping:
mapped_properties["pattern"] = self.format_pattern_mapping[format_type] mapped_properties["pattern"] = self.format_pattern_mapping[format_type]
if "examples" in mapped_properties: try:
mapped_properties["examples"] = [ if "examples" in mapped_properties:
self._parse_example(example, format_type, mapped_type) mapped_properties["examples"] = [
for example in 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: if "json_schema_extra" not in mapped_properties:
mapped_properties["json_schema_extra"] = {} mapped_properties["json_schema_extra"] = {}
mapped_properties["json_schema_extra"]["format"] = format_type mapped_properties["json_schema_extra"]["format"] = format_type
return mapped_type, mapped_properties 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

View File

@@ -5,7 +5,7 @@ from jambo.types import JSONSchema, RefCacheDict
from jsonschema.exceptions import SchemaError from jsonschema.exceptions import SchemaError
from jsonschema.validators import validator_for from jsonschema.validators import validator_for
from pydantic import BaseModel from pydantic import BaseModel
from typing_extensions import Optional from typing_extensions import MutableMapping, Optional
class SchemaConverter: class SchemaConverter:
@@ -17,8 +17,10 @@ class SchemaConverter:
fields and types. The generated model can be used for data validation and serialization. fields and types. The generated model can be used for data validation and serialization.
""" """
_namespace_registry: MutableMapping[str, RefCacheDict]
def __init__( def __init__(
self, namespace_registry: Optional[dict[str, RefCacheDict]] = None self, namespace_registry: Optional[MutableMapping[str, RefCacheDict]] = None
) -> None: ) -> None:
if namespace_registry is None: if namespace_registry is None:
namespace_registry = dict() namespace_registry = dict()
@@ -128,10 +130,8 @@ class SchemaConverter:
Gets a cached reference from the reference cache. Gets a cached reference from the reference cache.
:param ref_name: The name of the reference to get. :param ref_name: The name of the reference to get.
:return: The cached reference, or None if not found. :return: The cached reference, or None if not found.
""" """
cached_type = self._namespace_registry.get( cached_type = self._namespace_registry.get(namespace, {}).get(ref_name)
namespace, {}
).get(ref_name)
if isinstance(cached_type, type): if isinstance(cached_type, type):
return cached_type return cached_type

View File

@@ -17,6 +17,7 @@ classifiers = [
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
] ]
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"

View File

@@ -3,7 +3,6 @@ from jambo.parser import StringTypeParser
from pydantic import AnyUrl, EmailStr from pydantic import AnyUrl, EmailStr
import unittest
from datetime import date, datetime, time, timedelta, timezone from datetime import date, datetime, time, timedelta, timezone
from ipaddress import IPv4Address, IPv6Address, ip_address from ipaddress import IPv4Address, IPv6Address, ip_address
from unittest import TestCase from unittest import TestCase
@@ -121,7 +120,7 @@ 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, AnyUrl) 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): def test_string_parser_with_ip_formats(self):
parser = StringTypeParser() parser = StringTypeParser()
@@ -299,7 +298,6 @@ class TestStringTypeParser(TestCase):
}, },
) )
@unittest.skip("Duration parsing not yet implemented")
def test_string_parser_with_timedelta_format(self): def test_string_parser_with_timedelta_format(self):
parser = StringTypeParser() parser = StringTypeParser()
@@ -315,9 +313,9 @@ class TestStringTypeParser(TestCase):
self.assertEqual( self.assertEqual(
type_validator["examples"], type_validator["examples"],
[ [
timedelta(days=7), timedelta(days=428, hours=4, minutes=5, seconds=6),
timedelta(minutes=30), timedelta(minutes=30),
timedelta(hours=4, minutes=5, seconds=6), timedelta(days=7),
timedelta(seconds=0.5), timedelta(seconds=0.5),
], ],
) )

View File

@@ -1109,7 +1109,7 @@ class TestSchemaConverter(TestCase):
def test_namespace_isolation_via_on_call_config(self): def test_namespace_isolation_via_on_call_config(self):
namespace = "namespace1" namespace = "namespace1"
schema: JSONSchema = { schema: JSONSchema = {
"$id": namespace, "$id": namespace,
"title": "Person", "title": "Person",
@@ -1130,16 +1130,16 @@ class TestSchemaConverter(TestCase):
} }
model = self.converter.build_with_cache(schema) model = self.converter.build_with_cache(schema)
invalid_cached_model = self.converter.get_cached_ref("Person") invalid_cached_model = self.converter.get_cached_ref("Person")
self.assertIsNone(invalid_cached_model) self.assertIsNone(invalid_cached_model)
cached_model = self.converter.get_cached_ref("Person", namespace=namespace) cached_model = self.converter.get_cached_ref("Person", namespace=namespace)
self.assertIs(model, cached_model) self.assertIs(model, cached_model)
def test_clear_namespace_registry(self): def test_clear_namespace_registry(self):
namespace = "namespace_to_clear" namespace = "namespace_to_clear"
schema: JSONSchema = { schema: JSONSchema = {
"$id": namespace, "$id": namespace,
"title": "Person", "title": "Person",
@@ -1160,11 +1160,13 @@ class TestSchemaConverter(TestCase):
} }
model = self.converter.build_with_cache(schema) model = self.converter.build_with_cache(schema)
cached_model = self.converter.get_cached_ref("Person", namespace=namespace) cached_model = self.converter.get_cached_ref("Person", namespace=namespace)
self.assertIs(model, cached_model) self.assertIs(model, cached_model)
self.converter.clear_ref_cache(namespace=namespace) 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) self.assertIsNone(cleared_cached_model)

1254
uv.lock generated

File diff suppressed because it is too large Load Diff