From 1036c83ff8bcc7328061268f47e8a4cc3572a8d0 Mon Sep 17 00:00:00 2001 From: Robert Isele Date: Fri, 25 Apr 2025 14:00:54 +0200 Subject: [PATCH 1/7] Add initial version of RDF quad entities. --- .../dataintegration/typed_entities/quads.py | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 cmem_plugin_base/dataintegration/typed_entities/quads.py diff --git a/cmem_plugin_base/dataintegration/typed_entities/quads.py b/cmem_plugin_base/dataintegration/typed_entities/quads.py new file mode 100644 index 0000000..8fffff4 --- /dev/null +++ b/cmem_plugin_base/dataintegration/typed_entities/quads.py @@ -0,0 +1,217 @@ +"""Quad entities""" + +import abc +import uuid +from dataclasses import dataclass +from typing import ClassVar, cast + +from cmem_plugin_base.dataintegration.entity import Entity, EntityPath +from cmem_plugin_base.dataintegration.typed_entities import path_uri, type_uri +from cmem_plugin_base.dataintegration.typed_entities.typed_entities import ( + TypedEntitySchema, +) + +# --- RDF Node Types --- + +class RdfNode(abc.ABC): + """Abstract base class for an RDF node.""" + + type: ClassVar[str] + """The type code that identifies this RDF node type. Must be defined in subclasses.""" + + @property + @abc.abstractmethod + def value(self) -> str: + """The string representation of the node's value.""" + + +class ConcreteNode(RdfNode, abc.ABC): + """Abstract base class for an RdfNode which is either a Resource or a BlankNode.""" + + +@dataclass(frozen=True) +class Resource(ConcreteNode): + """Represents an RDF resource (typically a URI).""" + + type: ClassVar[str] = "URI" + + value: str + + +@dataclass(frozen=True) +class BlankNode(ConcreteNode): + """Represents an RDF blank node.""" + + type: ClassVar[str] = "BlankNode" + + value: str # Usually the identifier without the '_:' prefix internally + + +class Literal(RdfNode, abc.ABC): + """Abstract base class for an RDF literal.""" + + +@dataclass(frozen=True) +class PlainLiteral(Literal): + """Represents a plain literal without a language tag or datatype.""" + + type: ClassVar[str] = "Literal" + + value: str + +@dataclass(frozen=True) +class LanguageLiteral(Literal): + """Represents a literal with a language tag.""" + + type: ClassVar[str] = "LangLiteral" + + value: str + language: str + + +@dataclass(frozen=True) +class DataTypeLiteral(Literal): + """Represents a literal with a specific datatype.""" + + type: ClassVar[str] = "TypedLiteral" + + value: str + data_type: str # The datatype IRI (e.g., "http://www.w3.org/2001/XMLSchema#integer") + +@dataclass(frozen=True) +class Quad: + """Represents an RDF Quad.""" + + subject: ConcreteNode + predicate: Resource + object: RdfNode + graph: Resource | None + +def create_node(type_name: str, value: str, + language: str | None = None, data_type: str | None = None) -> RdfNode: + """Create an RDF node for a given type name.""" + match type_name: + case Resource.type: + return Resource(value) + case BlankNode.type: + return BlankNode(value) + case Literal.type: + return PlainLiteral(value) + case LanguageLiteral.type: + if language is None: + raise ValueError("Language must be provided for LanguageLiteral.") + return LanguageLiteral(value, language) + case DataTypeLiteral.type: + if data_type is None: + raise ValueError("Data type must be provided for DataTypeLiteral.") + return DataTypeLiteral(value, data_type) + case _: + raise ValueError(f"Unknown type: {type_name}") + +# --- RDF Quad Schema --- + + +class QuadEntitySchema(TypedEntitySchema[Quad]): + """Entity schema that holds a collection of RDF quads.""" + + def __init__(self): + super().__init__( + type_uri=type_uri("Quad"), + paths=[ + EntityPath(path_uri("quad/subject")), + EntityPath(path_uri("quad/subjectType")), + EntityPath(path_uri("quad/predicate")), + EntityPath(path_uri("quad/object")), + EntityPath(path_uri("quad/objectType")), + EntityPath(path_uri("quad/objectLanguage")), + EntityPath(path_uri("quad/objectDataType")), + EntityPath(path_uri("quad/graph")) + ], + ) + + def to_entity(self, quad: Quad) -> Entity: + """Create a generic entity from an RDF quad.""" + # Extract object language + match quad.object: + case LanguageLiteral(language=lang): + object_language = [lang] + case _: + object_language = [] + + # Extract object data type + match quad.object: + case DataTypeLiteral(data_type=dt): + object_data_type = [dt] + case _: + object_data_type = [] + + # Generate a UUID-based URI + uri_components = "".join([ + quad.subject.value, + quad.predicate.value, + quad.object.value, + object_language[0] if object_language else "", + object_data_type[0] if object_data_type else "", + quad.graph.value if quad.graph else "" + ]) + uri = f"urn:uuid:{uuid.uuid5(uuid.NAMESPACE_DNS, uri_components)}" + + # Build entity + return Entity( + uri=uri, + values=[ + [quad.subject.value], + [quad.subject.type], + [quad.predicate.value], + [quad.object.value], + [quad.object.type], + object_language, + object_data_type, + [quad.graph.value] if quad.graph else [] + ], + ) + + def from_entity(self, entity: Entity) -> Quad: + """Create an RDF quad entity from a generic entity.""" + # Indices for the values in the entity + subject_index = 0 + subject_type_index = 1 + predicate_index = 2 + object_index = 3 + object_type_index = 4 + object_language_index = 5 + object_data_type_index = 6 + graph_index = 7 + + # Get entity values + values = entity.values + + # Subject + subject_type_list = values[subject_type_index] + if len(subject_type_list) == 1: + subject = create_node(subject_type_list[0], values[subject_index][0]) + else: + raise ValueError(f"Invalid subject type: {subject_type_list}. Expected a single value.") + + # Predicate + predicate = Resource(values[predicate_index][0]) + + # Object + object_type_list = values[object_type_index] + if len(object_type_list) == 1: + lang_list = values[object_language_index] + lang = lang_list[0] if lang_list else None + type_list = values[object_data_type_index] + type_id = type_list[0] if type_list else None + object_value = create_node(object_type_list[0], values[object_index][0], lang, type_id) + else: + raise ValueError(f"Invalid object type: {object_type_list}. Expected a single element.") + + # Graph + graph_list = values[graph_index] + graph: Resource | None = None + if graph_list: + graph = Resource(graph_list[0]) + + # Build the Quad + return Quad(cast(ConcreteNode, subject), predicate, object_value, graph) From 7e43eaebb8c2661cb7175d524fdf464461ba27f1 Mon Sep 17 00:00:00 2001 From: Robert Isele Date: Mon, 5 May 2025 10:54:54 +0200 Subject: [PATCH 2/7] Improve TypedEntitySchema classes. --- .../dataintegration/typed_entities/file.py | 18 +++--- .../dataintegration/typed_entities/quads.py | 59 ++++++++++--------- .../typed_entities/typed_entities.py | 18 +++++- 3 files changed, 56 insertions(+), 39 deletions(-) diff --git a/cmem_plugin_base/dataintegration/typed_entities/file.py b/cmem_plugin_base/dataintegration/typed_entities/file.py index 10a122e..9cc473c 100644 --- a/cmem_plugin_base/dataintegration/typed_entities/file.py +++ b/cmem_plugin_base/dataintegration/typed_entities/file.py @@ -34,14 +34,16 @@ class FileEntitySchema(TypedEntitySchema[File]): """Entity schema that holds a collection of files.""" def __init__(self): - super().__init__( - type_uri=type_uri("File"), - paths=[ - EntityPath(path_uri("filePath")), - EntityPath(path_uri("fileType")), - EntityPath(path_uri("mimeType")), - ], - ) + # The parent class TypedEntitySchema implements a singleton pattern + if not hasattr(self, "_initialized"): + super().__init__( + type_uri=type_uri("File"), + paths=[ + EntityPath(path_uri("filePath")), + EntityPath(path_uri("fileType")), + EntityPath(path_uri("mimeType")), + ], + ) def to_entity(self, value: File) -> Entity: """Create a generic entity from a file""" diff --git a/cmem_plugin_base/dataintegration/typed_entities/quads.py b/cmem_plugin_base/dataintegration/typed_entities/quads.py index 8fffff4..d1904b4 100644 --- a/cmem_plugin_base/dataintegration/typed_entities/quads.py +++ b/cmem_plugin_base/dataintegration/typed_entities/quads.py @@ -13,32 +13,31 @@ # --- RDF Node Types --- +@dataclass() class RdfNode(abc.ABC): """Abstract base class for an RDF node.""" type: ClassVar[str] """The type code that identifies this RDF node type. Must be defined in subclasses.""" - @property - @abc.abstractmethod - def value(self) -> str: - """The string representation of the node's value.""" + value: str + """The value of the RDF node. This is typically a URI, a blank node identifier, or a literal.""" class ConcreteNode(RdfNode, abc.ABC): """Abstract base class for an RdfNode which is either a Resource or a BlankNode.""" -@dataclass(frozen=True) +@dataclass() class Resource(ConcreteNode): """Represents an RDF resource (typically a URI).""" type: ClassVar[str] = "URI" - value: str + value: str # The URI of the resource -@dataclass(frozen=True) +@dataclass() class BlankNode(ConcreteNode): """Represents an RDF blank node.""" @@ -51,7 +50,7 @@ class Literal(RdfNode, abc.ABC): """Abstract base class for an RDF literal.""" -@dataclass(frozen=True) +@dataclass() class PlainLiteral(Literal): """Represents a plain literal without a language tag or datatype.""" @@ -59,7 +58,8 @@ class PlainLiteral(Literal): value: str -@dataclass(frozen=True) + +@dataclass() class LanguageLiteral(Literal): """Represents a literal with a language tag.""" @@ -69,23 +69,25 @@ class LanguageLiteral(Literal): language: str -@dataclass(frozen=True) +@dataclass() class DataTypeLiteral(Literal): """Represents a literal with a specific datatype.""" type: ClassVar[str] = "TypedLiteral" value: str - data_type: str # The datatype IRI (e.g., "http://www.w3.org/2001/XMLSchema#integer") + data_type: str = "http://www.w3.org/2001/XMLSchema#string" # Default datatype IRI for literals + -@dataclass(frozen=True) +@dataclass() class Quad: """Represents an RDF Quad.""" subject: ConcreteNode predicate: Resource object: RdfNode - graph: Resource | None + graph: Resource | None = None + def create_node(type_name: str, value: str, language: str | None = None, data_type: str | None = None) -> RdfNode: @@ -95,7 +97,7 @@ def create_node(type_name: str, value: str, return Resource(value) case BlankNode.type: return BlankNode(value) - case Literal.type: + case PlainLiteral.type: return PlainLiteral(value) case LanguageLiteral.type: if language is None: @@ -110,24 +112,25 @@ def create_node(type_name: str, value: str, # --- RDF Quad Schema --- - class QuadEntitySchema(TypedEntitySchema[Quad]): """Entity schema that holds a collection of RDF quads.""" def __init__(self): - super().__init__( - type_uri=type_uri("Quad"), - paths=[ - EntityPath(path_uri("quad/subject")), - EntityPath(path_uri("quad/subjectType")), - EntityPath(path_uri("quad/predicate")), - EntityPath(path_uri("quad/object")), - EntityPath(path_uri("quad/objectType")), - EntityPath(path_uri("quad/objectLanguage")), - EntityPath(path_uri("quad/objectDataType")), - EntityPath(path_uri("quad/graph")) - ], - ) + # The parent class TypedEntitySchema implements a singleton pattern + if not hasattr(self, "_initialized"): + super().__init__( + type_uri=type_uri("Quad"), + paths=[ + EntityPath(path_uri("quad/subject")), + EntityPath(path_uri("quad/subjectType")), + EntityPath(path_uri("quad/predicate")), + EntityPath(path_uri("quad/object")), + EntityPath(path_uri("quad/objectType")), + EntityPath(path_uri("quad/objectLanguage")), + EntityPath(path_uri("quad/objectDataType")), + EntityPath(path_uri("quad/graph")) + ], + ) def to_entity(self, quad: Quad) -> Entity: """Create a generic entity from an RDF quad.""" diff --git a/cmem_plugin_base/dataintegration/typed_entities/typed_entities.py b/cmem_plugin_base/dataintegration/typed_entities/typed_entities.py index 5389e64..9aa0893 100644 --- a/cmem_plugin_base/dataintegration/typed_entities/typed_entities.py +++ b/cmem_plugin_base/dataintegration/typed_entities/typed_entities.py @@ -2,7 +2,7 @@ from abc import abstractmethod from collections.abc import Iterator, Sequence -from typing import Generic, TypeVar +from typing import ClassVar, Generic, TypeVar from cmem_plugin_base.dataintegration.entity import Entities, Entity, EntityPath, EntitySchema @@ -12,8 +12,20 @@ class TypedEntitySchema(EntitySchema, Generic[T]): """A custom entity schema that holds entities of a specific type (e.g. files).""" - def __init__(self, type_uri: str, paths: Sequence[EntityPath]): - super().__init__(type_uri, paths) + # Class variable to store singleton instances for each subclass + _instances: ClassVar[dict[type["TypedEntitySchema"], "TypedEntitySchema"]] = {} + + def __new__(cls, *args, **kwargs) -> "TypedEntitySchema": # noqa: ANN002, ANN003, ARG003 + """Implement singleton pattern for all subclasses of TypedEntitySchema.""" + if cls not in cls._instances: + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, type_uri: str, paths: Sequence[EntityPath]) -> None: + # Check if this instance has already been initialized + if not hasattr(self, "_initialized"): + super().__init__(type_uri, paths) + self._initialized = True @abstractmethod def to_entity(self, value: T) -> Entity: From bda06b23382872d450c6288e2778933df6c065c3 Mon Sep 17 00:00:00 2001 From: Robert Isele Date: Mon, 5 May 2025 10:55:12 +0200 Subject: [PATCH 3/7] Add test for RDF quads. --- tests/typed_entities/test_quads.py | 77 ++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/typed_entities/test_quads.py diff --git a/tests/typed_entities/test_quads.py b/tests/typed_entities/test_quads.py new file mode 100644 index 0000000..42ba47d --- /dev/null +++ b/tests/typed_entities/test_quads.py @@ -0,0 +1,77 @@ +"""Test for RDF quads.""" +import copy +import unittest +from collections.abc import Sequence + +from cmem_plugin_base.dataintegration.context import ExecutionContext +from cmem_plugin_base.dataintegration.entity import Entities +from cmem_plugin_base.dataintegration.plugins import WorkflowPlugin +from cmem_plugin_base.dataintegration.typed_entities.quads import ( + BlankNode, + DataTypeLiteral, + LanguageLiteral, + PlainLiteral, + Quad, + QuadEntitySchema, + Resource, +) + + +class ProcessQuadsOperator(WorkflowPlugin): + + def __init__(self) -> None: + pass + + def execute(self, inputs: Sequence[Entities], context: ExecutionContext) -> Entities: + quads = list(QuadEntitySchema().from_entities(inputs[0]).values) + quads[0].subject.value = "urn:instance:modified" + return QuadEntitySchema().to_entities(iter(quads)) + + +class QuadsTest(unittest.TestCase): + """Test for the typed entities feature.""" + + def test_quads(self) -> None: + """Test RDF quads.""" + # Creat quads with all supported node types + quads = [ + Quad( + subject = Resource("urn:instance:person1"), + predicate = Resource("urn:instance:hasCity"), + object = Resource("urn:instance:city1") + ), + Quad( + subject = Resource("urn:instance:person2"), + predicate = Resource("urn:instance:hasCity"), + object = PlainLiteral("Berlin") + ), + Quad( + subject = Resource("urn:instance:person3"), + predicate = Resource("urn:instance:hasCity"), + object = LanguageLiteral("Berlin", "en") + ), + Quad( + subject = Resource("urn:instance:person4"), + predicate = Resource("urn:instance:age"), + object = DataTypeLiteral("29", "http://www.w3.org/2001/XMLSchema#int") + ), + Quad( + subject = BlankNode("person5"), + predicate = Resource("urn:instance:hasCity"), + object = BlankNode("city1") + ) + ] + + # Execute operator + input_entities = QuadEntitySchema().to_entities(iter(quads)) + output = ProcessQuadsOperator().execute([input_entities], ExecutionContext()) + + # Check output + output_quads = list(QuadEntitySchema().from_entities(output).values) + expected_output_quads = copy.deepcopy(quads) + expected_output_quads[0].subject = Resource("urn:instance:modified") + assert output_quads == expected_output_quads + + +if __name__ == "__main__": + unittest.main() From d6286e73f7755677501957bc6ca4359e2c8d0e25 Mon Sep 17 00:00:00 2001 From: Robert Isele Date: Wed, 7 May 2025 09:40:15 +0200 Subject: [PATCH 4/7] Adapted FileEntitySchema so it can be used with datasets --- CHANGELOG.md | 6 ++++++ cmem_plugin_base/dataintegration/typed_entities/file.py | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cac825..2664a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/) +## [Unreleased] + +### Fixed + +- Adapted FileEntitySchema so it can be used with datasets (CMEM-6615). + ## [4.10.0] 2025-03-31 - shipped with DI v25.1.0 ### Added diff --git a/cmem_plugin_base/dataintegration/typed_entities/file.py b/cmem_plugin_base/dataintegration/typed_entities/file.py index 9cc473c..76bc723 100644 --- a/cmem_plugin_base/dataintegration/typed_entities/file.py +++ b/cmem_plugin_base/dataintegration/typed_entities/file.py @@ -39,9 +39,9 @@ def __init__(self): super().__init__( type_uri=type_uri("File"), paths=[ - EntityPath(path_uri("filePath")), - EntityPath(path_uri("fileType")), - EntityPath(path_uri("mimeType")), + EntityPath(path_uri("filePath"), is_single_value=True), + EntityPath(path_uri("fileType"), is_single_value=True), + EntityPath(path_uri("mimeType"), is_single_value=True), ], ) From 08076cd8ab99621554927568bc952a5cce74e8d0 Mon Sep 17 00:00:00 2001 From: Robert Isele Date: Tue, 13 May 2025 15:35:37 +0200 Subject: [PATCH 5/7] Migrated RDF quads to Pydantic --- .../dataintegration/typed_entities/quads.py | 40 ++--- poetry.lock | 163 +++++++++++++++++- pyproject.toml | 1 + tests/typed_entities/test_quads.py | 34 ++-- 4 files changed, 197 insertions(+), 41 deletions(-) diff --git a/cmem_plugin_base/dataintegration/typed_entities/quads.py b/cmem_plugin_base/dataintegration/typed_entities/quads.py index d1904b4..cf9a3e4 100644 --- a/cmem_plugin_base/dataintegration/typed_entities/quads.py +++ b/cmem_plugin_base/dataintegration/typed_entities/quads.py @@ -1,10 +1,10 @@ """Quad entities""" -import abc import uuid -from dataclasses import dataclass from typing import ClassVar, cast +from pydantic import BaseModel + from cmem_plugin_base.dataintegration.entity import Entity, EntityPath from cmem_plugin_base.dataintegration.typed_entities import path_uri, type_uri from cmem_plugin_base.dataintegration.typed_entities.typed_entities import ( @@ -13,8 +13,7 @@ # --- RDF Node Types --- -@dataclass() -class RdfNode(abc.ABC): +class RdfNode(BaseModel): """Abstract base class for an RDF node.""" type: ClassVar[str] @@ -24,11 +23,10 @@ class RdfNode(abc.ABC): """The value of the RDF node. This is typically a URI, a blank node identifier, or a literal.""" -class ConcreteNode(RdfNode, abc.ABC): +class ConcreteNode(RdfNode): """Abstract base class for an RdfNode which is either a Resource or a BlankNode.""" -@dataclass() class Resource(ConcreteNode): """Represents an RDF resource (typically a URI).""" @@ -37,7 +35,6 @@ class Resource(ConcreteNode): value: str # The URI of the resource -@dataclass() class BlankNode(ConcreteNode): """Represents an RDF blank node.""" @@ -46,11 +43,9 @@ class BlankNode(ConcreteNode): value: str # Usually the identifier without the '_:' prefix internally -class Literal(RdfNode, abc.ABC): +class Literal(RdfNode): """Abstract base class for an RDF literal.""" - -@dataclass() class PlainLiteral(Literal): """Represents a plain literal without a language tag or datatype.""" @@ -59,7 +54,6 @@ class PlainLiteral(Literal): value: str -@dataclass() class LanguageLiteral(Literal): """Represents a literal with a language tag.""" @@ -69,7 +63,6 @@ class LanguageLiteral(Literal): language: str -@dataclass() class DataTypeLiteral(Literal): """Represents a literal with a specific datatype.""" @@ -79,8 +72,7 @@ class DataTypeLiteral(Literal): data_type: str = "http://www.w3.org/2001/XMLSchema#string" # Default datatype IRI for literals -@dataclass() -class Quad: +class Quad(BaseModel): """Represents an RDF Quad.""" subject: ConcreteNode @@ -94,19 +86,19 @@ def create_node(type_name: str, value: str, """Create an RDF node for a given type name.""" match type_name: case Resource.type: - return Resource(value) + return Resource(value=value) case BlankNode.type: - return BlankNode(value) + return BlankNode(value=value) case PlainLiteral.type: - return PlainLiteral(value) + return PlainLiteral(value=value) case LanguageLiteral.type: if language is None: raise ValueError("Language must be provided for LanguageLiteral.") - return LanguageLiteral(value, language) + return LanguageLiteral(value=value, language=language) case DataTypeLiteral.type: if data_type is None: raise ValueError("Data type must be provided for DataTypeLiteral.") - return DataTypeLiteral(value, data_type) + return DataTypeLiteral(value=value, data_type=data_type) case _: raise ValueError(f"Unknown type: {type_name}") @@ -197,7 +189,7 @@ def from_entity(self, entity: Entity) -> Quad: raise ValueError(f"Invalid subject type: {subject_type_list}. Expected a single value.") # Predicate - predicate = Resource(values[predicate_index][0]) + predicate = Resource(value=values[predicate_index][0]) # Object object_type_list = values[object_type_index] @@ -214,7 +206,11 @@ def from_entity(self, entity: Entity) -> Quad: graph_list = values[graph_index] graph: Resource | None = None if graph_list: - graph = Resource(graph_list[0]) + graph = Resource(value=graph_list[0]) # Build the Quad - return Quad(cast(ConcreteNode, subject), predicate, object_value, graph) + return Quad( + subject=cast(ConcreteNode, subject), + predicate=predicate, + object=object_value, + graph=graph) diff --git a/poetry.lock b/poetry.lock index ecc8019..f3b11c1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,15 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] [[package]] name = "certifi" @@ -788,6 +799,138 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pydantic" +version = "2.11.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, + {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" version = "2.19.1" @@ -1174,6 +1317,20 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.0" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, + {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "uc-micro-py" version = "1.0.3" @@ -1209,6 +1366,6 @@ socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [metadata] -lock-version = "2.1" +lock-version = "2.0" python-versions = "^3.11" -content-hash = "431918334fbc3128859547762bb1fd7a50036766353ef13f6b4930d64df5546f" +content-hash = "e197931169def96cd8c485fc9645c4bc8e518f5c7ac966ecbe4544e194cb014c" diff --git a/pyproject.toml b/pyproject.toml index 1080f28..dad9aa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ homepage = "https://github.com/eccenca/cmem-plugin-base" python = "^3.11" cmem-cmempy = ">=23.3.0" python-ulid = "^2.2.0" +pydantic = "^2.11.4" [tool.poetry.group.dev.dependencies] genbadge = {extras = ["coverage"], version = "^1.1.1"} diff --git a/tests/typed_entities/test_quads.py b/tests/typed_entities/test_quads.py index 42ba47d..41f3fb8 100644 --- a/tests/typed_entities/test_quads.py +++ b/tests/typed_entities/test_quads.py @@ -18,11 +18,13 @@ class ProcessQuadsOperator(WorkflowPlugin): + """Test operator that modifies the subject of the first quad.""" def __init__(self) -> None: pass def execute(self, inputs: Sequence[Entities], context: ExecutionContext) -> Entities: + """Modify the subject of the first quad.""" quads = list(QuadEntitySchema().from_entities(inputs[0]).values) quads[0].subject.value = "urn:instance:modified" return QuadEntitySchema().to_entities(iter(quads)) @@ -36,29 +38,29 @@ def test_quads(self) -> None: # Creat quads with all supported node types quads = [ Quad( - subject = Resource("urn:instance:person1"), - predicate = Resource("urn:instance:hasCity"), - object = Resource("urn:instance:city1") + subject = Resource(value="urn:instance:person1"), + predicate = Resource(value="urn:instance:hasCity"), + object = Resource(value="urn:instance:city1") ), Quad( - subject = Resource("urn:instance:person2"), - predicate = Resource("urn:instance:hasCity"), - object = PlainLiteral("Berlin") + subject = Resource(value="urn:instance:person2"), + predicate = Resource(value="urn:instance:hasCity"), + object = PlainLiteral(value="Berlin") ), Quad( - subject = Resource("urn:instance:person3"), - predicate = Resource("urn:instance:hasCity"), - object = LanguageLiteral("Berlin", "en") + subject = Resource(value="urn:instance:person3"), + predicate = Resource(value="urn:instance:hasCity"), + object = LanguageLiteral(value="Berlin", language="en") ), Quad( - subject = Resource("urn:instance:person4"), - predicate = Resource("urn:instance:age"), - object = DataTypeLiteral("29", "http://www.w3.org/2001/XMLSchema#int") + subject = Resource(value="urn:instance:person4"), + predicate = Resource(value="urn:instance:age"), + object = DataTypeLiteral(value="29", data_type="http://www.w3.org/2001/XMLSchema#int") ), Quad( - subject = BlankNode("person5"), - predicate = Resource("urn:instance:hasCity"), - object = BlankNode("city1") + subject = BlankNode(value="person5"), + predicate = Resource(value="urn:instance:hasCity"), + object = BlankNode(value="city1") ) ] @@ -69,7 +71,7 @@ def test_quads(self) -> None: # Check output output_quads = list(QuadEntitySchema().from_entities(output).values) expected_output_quads = copy.deepcopy(quads) - expected_output_quads[0].subject = Resource("urn:instance:modified") + expected_output_quads[0].subject = Resource(value="urn:instance:modified") assert output_quads == expected_output_quads From 881bd5c34c45ab72682d243487f184eba2e31c22 Mon Sep 17 00:00:00 2001 From: Robert Isele Date: Tue, 13 May 2025 15:47:36 +0200 Subject: [PATCH 6/7] Fix noqa statements --- .../dataintegration/typed_entities/typed_entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmem_plugin_base/dataintegration/typed_entities/typed_entities.py b/cmem_plugin_base/dataintegration/typed_entities/typed_entities.py index 9aa0893..914b8db 100644 --- a/cmem_plugin_base/dataintegration/typed_entities/typed_entities.py +++ b/cmem_plugin_base/dataintegration/typed_entities/typed_entities.py @@ -15,7 +15,7 @@ class TypedEntitySchema(EntitySchema, Generic[T]): # Class variable to store singleton instances for each subclass _instances: ClassVar[dict[type["TypedEntitySchema"], "TypedEntitySchema"]] = {} - def __new__(cls, *args, **kwargs) -> "TypedEntitySchema": # noqa: ANN002, ANN003, ARG003 + def __new__(cls, *args, **kwargs) -> "TypedEntitySchema": # noqa: ANN002, ANN003, ARG004 """Implement singleton pattern for all subclasses of TypedEntitySchema.""" if cls not in cls._instances: cls._instances[cls] = super().__new__(cls) From cbdd889e774d37eceba31a741df085b05896469d Mon Sep 17 00:00:00 2001 From: Robert Isele Date: Tue, 13 May 2025 15:54:29 +0200 Subject: [PATCH 7/7] Ruff reformat files --- .../dataintegration/typed_entities/quads.py | 42 +++++++++++-------- tests/typed_entities/test_quads.py | 35 +++++++++------- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/cmem_plugin_base/dataintegration/typed_entities/quads.py b/cmem_plugin_base/dataintegration/typed_entities/quads.py index cf9a3e4..6a7051d 100644 --- a/cmem_plugin_base/dataintegration/typed_entities/quads.py +++ b/cmem_plugin_base/dataintegration/typed_entities/quads.py @@ -13,6 +13,7 @@ # --- RDF Node Types --- + class RdfNode(BaseModel): """Abstract base class for an RDF node.""" @@ -32,7 +33,7 @@ class Resource(ConcreteNode): type: ClassVar[str] = "URI" - value: str # The URI of the resource + value: str # The URI of the resource class BlankNode(ConcreteNode): @@ -40,12 +41,13 @@ class BlankNode(ConcreteNode): type: ClassVar[str] = "BlankNode" - value: str # Usually the identifier without the '_:' prefix internally + value: str # Usually the identifier without the '_:' prefix internally class Literal(RdfNode): """Abstract base class for an RDF literal.""" + class PlainLiteral(Literal): """Represents a plain literal without a language tag or datatype.""" @@ -69,7 +71,7 @@ class DataTypeLiteral(Literal): type: ClassVar[str] = "TypedLiteral" value: str - data_type: str = "http://www.w3.org/2001/XMLSchema#string" # Default datatype IRI for literals + data_type: str = "http://www.w3.org/2001/XMLSchema#string" # Default datatype IRI for literals class Quad(BaseModel): @@ -81,8 +83,9 @@ class Quad(BaseModel): graph: Resource | None = None -def create_node(type_name: str, value: str, - language: str | None = None, data_type: str | None = None) -> RdfNode: +def create_node( + type_name: str, value: str, language: str | None = None, data_type: str | None = None +) -> RdfNode: """Create an RDF node for a given type name.""" match type_name: case Resource.type: @@ -102,8 +105,10 @@ def create_node(type_name: str, value: str, case _: raise ValueError(f"Unknown type: {type_name}") + # --- RDF Quad Schema --- + class QuadEntitySchema(TypedEntitySchema[Quad]): """Entity schema that holds a collection of RDF quads.""" @@ -120,7 +125,7 @@ def __init__(self): EntityPath(path_uri("quad/objectType")), EntityPath(path_uri("quad/objectLanguage")), EntityPath(path_uri("quad/objectDataType")), - EntityPath(path_uri("quad/graph")) + EntityPath(path_uri("quad/graph")), ], ) @@ -141,14 +146,16 @@ def to_entity(self, quad: Quad) -> Entity: object_data_type = [] # Generate a UUID-based URI - uri_components = "".join([ - quad.subject.value, - quad.predicate.value, - quad.object.value, - object_language[0] if object_language else "", - object_data_type[0] if object_data_type else "", - quad.graph.value if quad.graph else "" - ]) + uri_components = "".join( + [ + quad.subject.value, + quad.predicate.value, + quad.object.value, + object_language[0] if object_language else "", + object_data_type[0] if object_data_type else "", + quad.graph.value if quad.graph else "", + ] + ) uri = f"urn:uuid:{uuid.uuid5(uuid.NAMESPACE_DNS, uri_components)}" # Build entity @@ -162,7 +169,7 @@ def to_entity(self, quad: Quad) -> Entity: [quad.object.type], object_language, object_data_type, - [quad.graph.value] if quad.graph else [] + [quad.graph.value] if quad.graph else [], ], ) @@ -210,7 +217,8 @@ def from_entity(self, entity: Entity) -> Quad: # Build the Quad return Quad( - subject=cast(ConcreteNode, subject), + subject=cast("ConcreteNode", subject), predicate=predicate, object=object_value, - graph=graph) + graph=graph, + ) diff --git a/tests/typed_entities/test_quads.py b/tests/typed_entities/test_quads.py index 41f3fb8..c17c52a 100644 --- a/tests/typed_entities/test_quads.py +++ b/tests/typed_entities/test_quads.py @@ -1,4 +1,5 @@ """Test for RDF quads.""" + import copy import unittest from collections.abc import Sequence @@ -38,30 +39,32 @@ def test_quads(self) -> None: # Creat quads with all supported node types quads = [ Quad( - subject = Resource(value="urn:instance:person1"), - predicate = Resource(value="urn:instance:hasCity"), - object = Resource(value="urn:instance:city1") + subject=Resource(value="urn:instance:person1"), + predicate=Resource(value="urn:instance:hasCity"), + object=Resource(value="urn:instance:city1"), ), Quad( - subject = Resource(value="urn:instance:person2"), - predicate = Resource(value="urn:instance:hasCity"), - object = PlainLiteral(value="Berlin") + subject=Resource(value="urn:instance:person2"), + predicate=Resource(value="urn:instance:hasCity"), + object=PlainLiteral(value="Berlin"), ), Quad( - subject = Resource(value="urn:instance:person3"), - predicate = Resource(value="urn:instance:hasCity"), - object = LanguageLiteral(value="Berlin", language="en") + subject=Resource(value="urn:instance:person3"), + predicate=Resource(value="urn:instance:hasCity"), + object=LanguageLiteral(value="Berlin", language="en"), ), Quad( - subject = Resource(value="urn:instance:person4"), - predicate = Resource(value="urn:instance:age"), - object = DataTypeLiteral(value="29", data_type="http://www.w3.org/2001/XMLSchema#int") + subject=Resource(value="urn:instance:person4"), + predicate=Resource(value="urn:instance:age"), + object=DataTypeLiteral( + value="29", data_type="http://www.w3.org/2001/XMLSchema#int" + ), ), Quad( - subject = BlankNode(value="person5"), - predicate = Resource(value="urn:instance:hasCity"), - object = BlankNode(value="city1") - ) + subject=BlankNode(value="person5"), + predicate=Resource(value="urn:instance:hasCity"), + object=BlankNode(value="city1"), + ), ] # Execute operator