From fcea994dd68ac07eb7c3f0d130aa1ca3c5268759 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Wed, 26 Nov 2025 15:05:10 -0300 Subject: [PATCH] feat: adds caching per namespace --- jambo/schema_converter.py | 32 +++++--- tests/test_schema_converter.py | 129 ++++++++++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 11 deletions(-) diff --git a/jambo/schema_converter.py b/jambo/schema_converter.py index 37a062d..0e92119 100644 --- a/jambo/schema_converter.py +++ b/jambo/schema_converter.py @@ -17,10 +17,12 @@ class SchemaConverter: fields and types. The generated model can be used for data validation and serialization. """ - def __init__(self, ref_cache: Optional[RefCacheDict] = None) -> None: - if ref_cache is None: - ref_cache = dict() - self._ref_cache = ref_cache + def __init__( + self, namespace_registry: Optional[dict[str, RefCacheDict]] = None + ) -> None: + if namespace_registry is None: + namespace_registry = dict() + self._namespace_registry = namespace_registry def build_with_cache( self, @@ -43,7 +45,8 @@ class SchemaConverter: if without_cache: local_ref_cache = dict() elif ref_cache is None: - local_ref_cache = self._ref_cache + namespace = schema.get("$id", "default") + local_ref_cache = self._namespace_registry.setdefault(namespace, dict()) else: local_ref_cache = ref_cache @@ -107,19 +110,28 @@ class SchemaConverter: unsupported_field=unsupported_type, ) - def clear_ref_cache(self) -> None: + def clear_ref_cache(self, namespace: Optional[str] = "default") -> None: """ Clears the reference cache. """ - self._ref_cache.clear() + if namespace is None: + self._namespace_registry.clear() + return - def get_cached_ref(self, ref_name: str): + if namespace in self._namespace_registry: + self._namespace_registry[namespace].clear() + + def get_cached_ref( + self, ref_name: str, namespace: str = "default" + ) -> Optional[type]: """ 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._ref_cache.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/test_schema_converter.py b/tests/test_schema_converter.py index 450e441..b8da4d1 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -877,7 +877,6 @@ class TestSchemaConverter(TestCase): converter2 = SchemaConverter(ref_cache) model2 = converter2.build_with_cache(schema) - self.assertIs(converter1._ref_cache, converter2._ref_cache) self.assertIs(model1, model2) def test_instance_level_ref_cache_isolation_via_without_cache_param(self): @@ -1041,3 +1040,131 @@ class TestSchemaConverter(TestCase): with self.assertRaises(InvalidSchemaException): self.converter.build_with_cache(schema) + + def tests_instance_level_ref_cache_isolation_via_property_id(self): + schema1: JSONSchema = { + "$id": "http://example.com/schemas/person1.json", + "title": "Person", + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "emergency_contact": { + "$ref": "#", + }, + }, + "required": ["name", "age"], + } + + model1 = self.converter.build_with_cache(schema1) + + schema2: JSONSchema = { + "$id": "http://example.com/schemas/person2.json", + "title": "Person", + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "address": {"type": "string"}, + }, + "required": ["name", "age", "address"], + } + + model2 = self.converter.build_with_cache(schema2) + + self.assertIsNot(model1, model2) + + def tests_instance_level_ref_cache_colision_when_same_property_id(self): + schema1: JSONSchema = { + "$id": "http://example.com/schemas/person.json", + "title": "Person", + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "emergency_contact": { + "$ref": "#", + }, + }, + "required": ["name", "age"], + } + + model1 = self.converter.build_with_cache(schema1) + + schema2: JSONSchema = { + "$id": "http://example.com/schemas/person.json", + "title": "Person", + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "address": {"type": "string"}, + }, + "required": ["name", "age", "address"], + } + + model2 = self.converter.build_with_cache(schema2) + + self.assertIs(model1, model2) + + def test_namespace_isolation_via_on_call_config(self): + namespace = "namespace1" + + schema: JSONSchema = { + "$id": namespace, + "title": "Person", + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "address": { + "type": "object", + "properties": { + "street": {"type": "string"}, + "city": {"type": "string"}, + }, + "required": ["street", "city"], + }, + }, + "required": ["name", "age", "address"], + } + + 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", + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"}, + "address": { + "type": "object", + "properties": { + "street": {"type": "string"}, + "city": {"type": "string"}, + }, + "required": ["street", "city"], + }, + }, + "required": ["name", "age", "address"], + } + + 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) + self.assertIsNone(cleared_cached_model)