From 0eaacc49597ce9bb83a32887013fe745272023cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Fri, 7 Nov 2025 12:47:11 +0100 Subject: [PATCH 01/36] Format all files using black formatter --- graph_db_interface/exceptions.py | 6 +-- graph_db_interface/queries/triple_multi.py | 8 +++- graph_db_interface/queries/triple_single.py | 4 +- graph_db_interface/sparql_query.py | 8 +++- .../utils/graph_db_credentials.py | 24 ++++++----- graph_db_interface/utils/pretty_print.py | 41 +++++++++++++++---- 6 files changed, 64 insertions(+), 27 deletions(-) diff --git a/graph_db_interface/exceptions.py b/graph_db_interface/exceptions.py index 08d34af..535ef0f 100644 --- a/graph_db_interface/exceptions.py +++ b/graph_db_interface/exceptions.py @@ -42,11 +42,11 @@ class InvalidIRIError(GraphDBInterfaceError): def __init__(self, message: str): self.message = message super().__init__(self.message) - - + + class GraphDbException(GraphDBInterfaceError): """Exception raised for general GraphDB errors.""" - + def __init__(self, message: str): self.message = message super().__init__(self.message) diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index d801905..121d052 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -142,7 +142,9 @@ def triples_add( return False ask_result = self.query(query=ask_query_string, update=False) if ask_result is not None and ask_result["boolean"]: - self.logger.warning("One of the triples to add already exists in the graph.") + self.logger.warning( + "One of the triples to add already exists in the graph." + ) return False query = SPARQLQuery( @@ -269,7 +271,9 @@ def triples_update( sub_old, pred_old, obj_old = triple if check_exist: if not self.triple_exists(sub_old, pred_old, obj_old): - self.logger.warning(f"Triple does not exist: {sub_old} {pred_old} {obj_old}") + self.logger.warning( + f"Triple does not exist: {sub_old} {pred_old} {obj_old}" + ) return False sub_old = utils.prepare_subject(sub_old, ensure_iri=True) diff --git a/graph_db_interface/queries/triple_single.py b/graph_db_interface/queries/triple_single.py index 3ff1ead..56a517f 100644 --- a/graph_db_interface/queries/triple_single.py +++ b/graph_db_interface/queries/triple_single.py @@ -221,7 +221,9 @@ def triple_update( pred_old, obj_old, ): - self.logger.warning(f"Triple does not exist: {sub_old} {pred_old} {obj_old}") + self.logger.warning( + f"Triple does not exist: {sub_old} {pred_old} {obj_old}" + ) return False if sub_new is not None: diff --git a/graph_db_interface/sparql_query.py b/graph_db_interface/sparql_query.py index c915f78..bf5ce5f 100644 --- a/graph_db_interface/sparql_query.py +++ b/graph_db_interface/sparql_query.py @@ -2,6 +2,7 @@ from typing import List, Optional, Dict, Tuple from graph_db_interface.utils import utils + class SPARQLQueryType(Enum): """Enum for different SPARQL query types.""" @@ -16,6 +17,7 @@ class SPARQLQueryType(Enum): DELETE_DATA = "DELETE DATA" DELETE_INSERT = "DELETE/INSERT" + class SPARQLQuery: def __init__( self, @@ -103,7 +105,9 @@ def add_insert_exists_block( """ ) block = "\n".join(block_parts) - self._query_blocks.append({"type": SPARQLQueryType.INSERT_EXISTS, "data": block}) + self._query_blocks.append( + {"type": SPARQLQueryType.INSERT_EXISTS, "data": block} + ) def add_delete_data_block( self, @@ -199,4 +203,4 @@ def to_string(self, validate: bool = True) -> str: ): # Validate the update query utils.validate_update_query(query) - return query \ No newline at end of file + return query diff --git a/graph_db_interface/utils/graph_db_credentials.py b/graph_db_interface/utils/graph_db_credentials.py index 43aeb7e..81a3217 100644 --- a/graph_db_interface/utils/graph_db_credentials.py +++ b/graph_db_interface/utils/graph_db_credentials.py @@ -1,6 +1,7 @@ from dataclasses import dataclass import os + @dataclass(frozen=True) class GraphDBCredentials: """ @@ -11,8 +12,9 @@ class GraphDBCredentials: username (str): The username for authenticating with the database. password (str): The password for authenticating with the database. database_name (str): The name of the specific database to connect to. - + """ + base_url: str username: str password: str @@ -20,7 +22,7 @@ class GraphDBCredentials: @classmethod def from_env(cls): - ''' + """ Create a GraphDB instance using environment variables. The following environment variables must be set: - `GRAPHDB_USERNAME`: The username for GraphDB authentication. - `GRAPHDB_PASSWORD`: The password for GraphDB authentication. @@ -29,7 +31,7 @@ def from_env(cls): Raises: ValueError: If any of the required environment variables are not set. - ''' + """ if os.getenv("GRAPHDB_USERNAME") is None: raise ValueError("GRAPHDB_USERNAME environment variable is not set.") @@ -49,13 +51,15 @@ def from_env(cls): base_url=base_url, username=username, password=password, - repository=repository + repository=repository, ) def __iter__(self): - return iter(( - self.base_url, - self.username, - self.password, - self.repository, - )) \ No newline at end of file + return iter( + ( + self.base_url, + self.username, + self.password, + self.repository, + ) + ) diff --git a/graph_db_interface/utils/pretty_print.py b/graph_db_interface/utils/pretty_print.py index 8c57780..c868725 100644 --- a/graph_db_interface/utils/pretty_print.py +++ b/graph_db_interface/utils/pretty_print.py @@ -2,7 +2,14 @@ from typing import Any, Optional import json -def format_result(result: list[tuple[str]] | tuple[tuple[str, ...], ...] | dict[Any, Any] | list[dict], variables: Optional[list[str]] = None, grouping_variables: Optional[list[str]] = None) -> str: + +def format_result( + result: ( + list[tuple[str]] | tuple[tuple[str, ...], ...] | dict[Any, Any] | list[dict] + ), + variables: Optional[list[str]] = None, + grouping_variables: Optional[list[str]] = None, +) -> str: """Return a human-readable string for SPARQL query results. This utility formats different shapes of results produced by SPARQL SELECT queries @@ -43,13 +50,18 @@ def format_result(result: list[tuple[str]] | tuple[tuple[str, ...], ...] | dict[ out += f"({', '.join(v for v in variables)})" return out - if isinstance(result, list) and result and isinstance(result[0], dict): # raw JSON bindings + if ( + isinstance(result, list) and result and isinstance(result[0], dict) + ): # raw JSON bindings return f"Result: {_format_raw_json_bindings(result)}" - elif isinstance(result, dict): # nested structure - return f"Result: {_format_nested_structure(result, variables, grouping_variables)}" - else: # tuple of tuples or tuple of scalars + elif isinstance(result, dict): # nested structure + return ( + f"Result: {_format_nested_structure(result, variables, grouping_variables)}" + ) + else: # tuple of tuples or tuple of scalars return f"Result: {_format_entry(result, variables)}" + def _format_raw_json_bindings(result: list[dict]) -> str: """Format raw JSON bindings (list of dicts) as pretty-printed JSON. @@ -69,7 +81,13 @@ def _format_raw_json_bindings(result: list[dict]) -> str: rendered_items.append(entries) return "[{" + "},\n\t {".join(rendered_items) + "}]" -def _format_nested_structure(structure, variables: Optional[list[str]], grouping_variables: Optional[list[str]], level: int = 0) -> str: + +def _format_nested_structure( + structure, + variables: Optional[list[str]], + grouping_variables: Optional[list[str]], + level: int = 0, +) -> str: """Format a nested dictionary result structure with indentation. Parameters: @@ -90,11 +108,16 @@ def _format_nested_structure(structure, variables: Optional[list[str]], grouping return "{}" items = [] for key, value in structure.items(): - formatted_value = _format_nested_structure(value, variables, grouping_variables, level + 1) + formatted_value = _format_nested_structure( + value, variables, grouping_variables, level + 1 + ) items.append(f"{indent} {key}: {formatted_value}") - return f"{{ # {grouping_variables[level]}\n" + ",\n".join(items) + f"\n{indent}}}" + return ( + f"{{ # {grouping_variables[level]}\n" + ",\n".join(items) + f"\n{indent}}}" + ) return f"\n {_format_entry(structure, variables, indent[:-1])}" + def _format_entry(structure: tuple, variables: list[str], indent: str = "") -> str: """Format a flat tuple or tuple-of-tuples as aligned columns. @@ -123,5 +146,5 @@ def _format_entry(structure: tuple, variables: list[str], indent: str = "") -> s lmax[i] = max(lmax[i], len(re)) var_names = " ".join(f"{re:<{lmax[i]}}" for i, re in enumerate(variables)) rows = [" , ".join(f"{re:<{lmax[i]}}" for i, re in enumerate(s)) for s in structure] - content = f' ),\n{indent} ( '.join(rows) + content = f" ),\n{indent} ( ".join(rows) return f"{indent}# {var_names}\n{indent} (( {content} ))" From 9233c0b200c4acfc26a75e1df0d4333bf77b901f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 1 Dec 2025 19:03:29 +0100 Subject: [PATCH 02/36] Modify existing tests - `triple_add`, `triple_delete`, `triple_update` now take triple as tuple instead of individual items - Making use of to `Triple` class, more consistent with multi-triple methods - Prefix management is moved from GraphDB to IRI class - `import_statements`, `clear_graph` now return success bool - preparation for generalization to non-web based triple stores - `fetch_statements` now returns Optional[Graph], None if failed - Drop support of prefixing for non-full hosts (e.g. `onto`: `http://www.ontotext.com/`), is To Be Added if required in the future - Drop support of substring searches - IRIs are treated as atomic, and verified at generation time - Major cleanup of utils, as many methods are made redundant - `encapsulate_named_graph` now takes IRI, no additional type checking as is only used internally - Switch pytest fixture to `Tests` repository as overriding variable - Clear used graphs at the start of every run --- tests/conftest.py | 20 ++- tests/test_graph_manipulation.py | 170 ++++++++------------------ tests/test_graph_store.py | 110 ++++++++--------- tests/test_triple_get.py | 94 ++++++-------- tests/test_utils.py | 203 ++----------------------------- 5 files changed, 176 insertions(+), 421 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 933971d..4887b62 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ -import json import os import sys import pytest from graph_db_interface import GraphDB, GraphDBCredentials +REPOSITORY = "Tests" + @pytest.fixture(scope="session") def db() -> GraphDB: @@ -18,6 +19,19 @@ def db() -> GraphDB: print(f"Missing environment variable '{env_var}'.", file=sys.stderr) sys.exit(1) - credentials = GraphDBCredentials.from_env() + credentials = GraphDBCredentials( + base_url=os.getenv("GRAPHDB_URL"), + username=os.getenv("GRAPHDB_USERNAME"), + password=os.getenv("GRAPHDB_PASSWORD"), + repository=REPOSITORY or os.getenv("GRAPHDB_REPOSITORY"), + ) + + db = GraphDB(credentials=credentials) + + for graph in [ + None, + "http://example.org/named_graph", + ]: + db.clear_graph(graph) - return GraphDB(credentials=credentials) + return db diff --git a/tests/test_graph_manipulation.py b/tests/test_graph_manipulation.py index f09113d..2b41a78 100644 --- a/tests/test_graph_manipulation.py +++ b/tests/test_graph_manipulation.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Tuple, List, Union import pytest from rdflib import Literal, XSD from graph_db_interface import GraphDB @@ -7,29 +7,26 @@ NAMED_GRAPH = "http://example.org/named_graph" -SUBJECT_1 = "" -PREDICATE_1 = "" -OBJECT_1 = Literal(0.5, datatype=XSD.double) # data value +SUBJECT_1 = "http://example.org#subject_1" +PREDICATE_1 = "http://example.org#predicate_1" +OBJECT_1 = 0.5 -SUBJECT_2 = "" -PREDICATE_2 = "" -OBJECT_2 = Literal(42, datatype=XSD.integer) # data value +SUBJECT_2 = "http://example.org#subject_2" +PREDICATE_2 = "http://example.org#predicate_2" +OBJECT_2 = 42 -NEW_SUBJECT_1 = "" -NEW_PREDICATE_1 = "" +NEW_SUBJECT_1 = "http://example.org#new_subject" +NEW_PREDICATE_1 = "http://example.org#new_predicate" NEW_OBJECT_1 = Literal('string with "quotes"', datatype=XSD.string) -NEW_SUBJECT_2 = "" -NEW_PREDICATE_2 = "" -NEW_OBJECT_2 = Literal(True, datatype=XSD.boolean) +NEW_SUBJECT_2 = "http://example.org#new_subject_2" +NEW_PREDICATE_2 = "http://example.org#new_predicate_2" +NEW_OBJECT_2 = True -from typing import Any - -from typing import List, Union LIST_OF_TRIPLES: List[Tuple[str, str, Union[str, Literal]]] = [ - (SUBJECT_1, PREDICATE_1, OBJECT_1), + ((SUBJECT_1, PREDICATE_1, OBJECT_1)), (SUBJECT_2, PREDICATE_2, OBJECT_2), ] LIST_OF_NEW_TRIPLES: List[Tuple[str, str, Union[str, Literal]]] = [ @@ -47,19 +44,19 @@ def setup(request, db: GraphDB): def test_add_and_delete_triple(db: GraphDB): # Add a new triple - result = db.triple_add(SUBJECT_1, PREDICATE_1, OBJECT_1) + result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) assert result is True # try to delete the triple - result = db.triple_delete(SUBJECT_1, PREDICATE_1, OBJECT_1) + result = db.triple_delete((SUBJECT_1, PREDICATE_1, OBJECT_1)) assert result is True # try to delete the triple again - result = db.triple_delete(SUBJECT_1, PREDICATE_1, OBJECT_1) + result = db.triple_delete((SUBJECT_1, PREDICATE_1, OBJECT_1)) assert result is False # if we dont check for existence it shoud return True - result = db.triple_delete(SUBJECT_1, PREDICATE_1, OBJECT_1, check_exist=False) + result = db.triple_delete((SUBJECT_1, PREDICATE_1, OBJECT_1), check_exist=False) assert result is True @@ -83,86 +80,78 @@ def test_add_and_delete_multiple_triples(db: GraphDB): def test_update_triple(db: GraphDB): # Add a new triple - result = db.triple_add(SUBJECT_1, PREDICATE_1, OBJECT_1) + result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) assert result is True # Input errors with pytest.raises(InvalidInputError): - db.triple_update(sub_old=SUBJECT_1, pred_old=PREDICATE_1, obj_old=None) + db.triple_update((SUBJECT_1, PREDICATE_1, None)) # Nothing to update with pytest.raises(InvalidInputError): - db.triple_update(sub_old=SUBJECT_1, pred_old=PREDICATE_1, obj_old=OBJECT_1) + db.triple_update((SUBJECT_1, PREDICATE_1, OBJECT_1)) # try to update the full triple and change its object result = db.triple_update( - sub_old=SUBJECT_1, - pred_old=PREDICATE_1, - obj_old=OBJECT_1, - sub_new=NEW_SUBJECT_1, - pred_new=NEW_PREDICATE_1, - obj_new=NEW_OBJECT_1, + (SUBJECT_1, PREDICATE_1, OBJECT_1), + new_sub=NEW_SUBJECT_1, + new_pred=NEW_PREDICATE_1, + new_obj=NEW_OBJECT_1, ) assert result is True - result = db.triple_delete(NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1) + result = db.triple_delete((NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1)) assert result is True def test_update_triple_only_subject(db: GraphDB): # Add a new triple - result = db.triple_add(SUBJECT_1, PREDICATE_1, OBJECT_1) + result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) assert result is True # only update the subject of the triple result = db.triple_update( - sub_old=SUBJECT_1, - pred_old=PREDICATE_1, - obj_old=OBJECT_1, - sub_new=NEW_SUBJECT_1, + (SUBJECT_1, PREDICATE_1, OBJECT_1), + new_sub=NEW_SUBJECT_1, ) assert result is True # try to delete the triple - result = db.triple_delete(NEW_SUBJECT_1, PREDICATE_1, OBJECT_1) + result = db.triple_delete((NEW_SUBJECT_1, PREDICATE_1, OBJECT_1)) assert result is True def test_update_triple_only_predicate(db: GraphDB): # Add a new triple - result = db.triple_add(SUBJECT_1, PREDICATE_1, OBJECT_1) + result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) assert result is True # only update the predicate of the triple result = db.triple_update( - sub_old=SUBJECT_1, - pred_old=PREDICATE_1, - obj_old=OBJECT_1, - pred_new=NEW_PREDICATE_1, + (SUBJECT_1, PREDICATE_1, OBJECT_1), + new_pred=NEW_PREDICATE_1, ) assert result is True # try to delete the triple - result = db.triple_delete(SUBJECT_1, NEW_PREDICATE_1, OBJECT_1) + result = db.triple_delete((SUBJECT_1, NEW_PREDICATE_1, OBJECT_1)) assert result is True def test_update_triple_only_object(db: GraphDB): # Add a new triple - result = db.triple_add(SUBJECT_1, PREDICATE_1, OBJECT_1) + result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) assert result is True # only update the object of the triple result = db.triple_update( - sub_old=SUBJECT_1, - pred_old=PREDICATE_1, - obj_old=OBJECT_1, - obj_new=NEW_OBJECT_1, + (SUBJECT_1, PREDICATE_1, OBJECT_1), + new_obj=NEW_OBJECT_1, ) assert result is True # try to delete the triple - result = db.triple_delete(SUBJECT_1, PREDICATE_1, NEW_OBJECT_1) + result = db.triple_delete((SUBJECT_1, PREDICATE_1, NEW_OBJECT_1)) assert result is True @@ -185,7 +174,7 @@ def test_update_multiple_triples(db: GraphDB): def test_iri_exists(db: GraphDB): # add a new triple to the default graph - result = db.triple_add(SUBJECT_1, PREDICATE_1, OBJECT_1) + result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) assert result is True # does not specify any part of a triple to look for @@ -223,31 +212,31 @@ def test_iri_exists(db: GraphDB): ) assert result is False - result = db.triple_delete(SUBJECT_1, PREDICATE_1, OBJECT_1) + result = db.triple_delete((SUBJECT_1, PREDICATE_1, OBJECT_1)) assert result is True def test_convenience_functions(db: GraphDB): - subject = "" + subject = "http://example.org#instance" predicate = "rdfs:subClassOf" - object = "" + object = "http://example.org#myClass" - result = db.triple_add(subject, predicate, object) + result = db.triple_add((subject, predicate, object)) assert result is True - result = db.triple_add(subject, "rdf:type", "owl:NamedIndividual") + result = db.triple_add((subject, "rdf:type", "owl:NamedIndividual")) assert result is True - result = db.triple_add(subject, "rdf:type", object) + result = db.triple_add((subject, "rdf:type", object)) assert result is True - result = db.triple_add(object, "rdf:type", "owl:Class") + result = db.triple_add((object, "rdf:type", "owl:Class")) assert result is True result = db.is_subclass(subject, object) assert result is True - result = db.is_subclass(subject, "") + result = db.is_subclass(subject, "http://example.org#someNonExistingClass") assert result is False result = db.owl_is_named_individual(subject) @@ -257,7 +246,7 @@ def test_convenience_functions(db: GraphDB): assert result is False classes = db.owl_get_classes_of_individual(subject, local_name=False) - assert classes == [utils.strip_angle_brackets(object)] + assert classes == [object] classes = db.owl_get_classes_of_individual(subject, local_name=True) assert classes == [utils.get_local_name(object)] @@ -265,71 +254,14 @@ def test_convenience_functions(db: GraphDB): classes = db.owl_get_classes_of_individual(object, local_name=False) assert classes == [] - result = db.triple_delete(subject, predicate, object) - assert result is True - - result = db.triple_delete(subject, "rdf:type", "owl:NamedIndividual") + result = db.triple_delete((subject, predicate, object)) assert result is True - result = db.triple_delete(subject, "rdf:type", object) + result = db.triple_delete((subject, "rdf:type", "owl:NamedIndividual")) assert result is True - result = db.triple_delete(object, "rdf:type", "owl:Class") - assert result is True - - -def test_prefix_management(db: GraphDB): - """Test add_prefix, remove_prefix, and get_prefixes functionality""" - - # Get initial prefixes (should include default prefixes: owl, rdf, rdfs, onto) - initial_prefixes = db.get_prefixes() - assert isinstance(initial_prefixes, dict) - assert "owl" in initial_prefixes - assert "rdf" in initial_prefixes - assert "rdfs" in initial_prefixes - assert "onto" in initial_prefixes - initial_count = len(initial_prefixes) - - # Test add_prefix with full IRI - db.add_prefix("ex", "") - prefixes = db.get_prefixes() - assert "ex" in prefixes - assert prefixes["ex"] == "" - assert len(prefixes) == initial_count + 1 - - # Test add_prefix with IRI without angle brackets (should add them) - db.add_prefix("test", "http://test.org/") - prefixes = db.get_prefixes() - assert "test" in prefixes - assert prefixes["test"] == "" - assert len(prefixes) == initial_count + 2 - - # Test overwriting an existing prefix - db.add_prefix("ex", "") - prefixes = db.get_prefixes() - assert prefixes["ex"] == "" - assert len(prefixes) == initial_count + 2 # Count shouldn't increase - - # Test remove_prefix for existing prefix - result = db.remove_prefix("ex") + result = db.triple_delete((subject, "rdf:type", object)) assert result is True - prefixes = db.get_prefixes() - assert "ex" not in prefixes - assert len(prefixes) == initial_count + 1 - # Test remove_prefix for non-existing prefix - result = db.remove_prefix("nonexistent") - assert result is False - - # Test remove_prefix for another existing prefix - result = db.remove_prefix("test") + result = db.triple_delete((object, "rdf:type", "owl:Class")) assert result is True - prefixes = db.get_prefixes() - assert "test" not in prefixes - assert len(prefixes) == initial_count - - # Verify default prefixes are still intact - assert "owl" in prefixes - assert "rdf" in prefixes - assert "rdfs" in prefixes - assert "onto" in prefixes diff --git a/tests/test_graph_store.py b/tests/test_graph_store.py index d9a8c2f..ea3df9d 100644 --- a/tests/test_graph_store.py +++ b/tests/test_graph_store.py @@ -25,147 +25,147 @@ foaf:name "Peter" . """ -GRAPH_URI = "http://test_named_graph" +GRAPH_IRI = "http://test_named_graph" def test_named_graph(db: GraphDB): # first fetch the test named graph to ensure it is empty - response, graph = db.fetch_statements( - graph_uri=GRAPH_URI, + graph = db.fetch_statements( + graph_iri=GRAPH_IRI, ) - assert response.status_code == 200 + assert graph is not None assert len(graph) == 0 # now we add some data to it - response = db.import_statements( + success = db.import_statements( content=TEST_TTL_DATA, overwrite=False, - graph_uri=GRAPH_URI, + graph_iri=GRAPH_IRI, content_type="application/x-turtle", ) - - assert response.status_code == 204 + assert success # fetch the named graph again to ensure data was added - response, graph = db.fetch_statements( - graph_uri=GRAPH_URI, + graph = db.fetch_statements( + graph_iri=GRAPH_IRI, ) - assert response.status_code == 200 + assert graph is not None assert len(graph) == 5 # now we import new data with overwrite=True - response = db.import_statements( + success = db.import_statements( content=UPDATED_TTL_DATA, overwrite=True, - graph_uri=GRAPH_URI, + graph_iri=GRAPH_IRI, content_type="application/x-turtle", ) - assert response.status_code == 204 + assert success # fetch the named graph again to ensure data was updated - response, graph = db.fetch_statements( - graph_uri=GRAPH_URI, + graph = db.fetch_statements( + graph_iri=GRAPH_IRI, ) - assert response.status_code == 200 + assert graph is not None assert len(graph) == 7 # now we add data again with overwrite=False - response = db.import_statements( + success = db.import_statements( content=TEST_TTL_DATA, overwrite=False, - graph_uri=GRAPH_URI, + graph_iri=GRAPH_IRI, content_type="application/x-turtle", ) - assert response.status_code == 204 + assert success # fetch the named graph again to ensure data was appended - response, graph = db.fetch_statements( - graph_uri=GRAPH_URI, + graph = db.fetch_statements( + graph_iri=GRAPH_IRI, ) - assert response.status_code == 200 + assert graph is not None assert len(graph) == 9 # clear the named graph - response = db.clear_graph( - graph_uri=GRAPH_URI, + success = db.clear_graph( + graph_iri=GRAPH_IRI, ) - assert response.status_code == 204 + assert success # fetch the named graph again to ensure it is empty - response, graph = db.fetch_statements( - graph_uri=GRAPH_URI, + graph = db.fetch_statements( + graph_iri=GRAPH_IRI, ) - assert response.status_code == 200 + assert graph is not None assert len(graph) == 0 def test_default_graph(db: GraphDB): # first fetch the default graph - response, graph = db.fetch_statements( - graph_uri=None, + graph = db.fetch_statements( + graph_iri=None, ) - assert response.status_code == 200 + assert graph is not None triples_in_default_graph = int(len(graph)) print(f"Default graph has {triples_in_default_graph} triples.") # now we add some data to it - response = db.import_statements( + success = db.import_statements( content=TEST_TTL_DATA, overwrite=False, - graph_uri=None, + graph_iri=None, content_type="application/x-turtle", ) + assert success expected_num_triples = triples_in_default_graph + 5 # fetch the default graph again to ensure data was added - response, graph = db.fetch_statements( - graph_uri=None, + graph = db.fetch_statements( + graph_iri=None, ) - assert response.status_code == 200 + assert graph is not None assert int(len(graph)) == int(expected_num_triples) # now we import new data with overwrite=True - response = db.import_statements( + success = db.import_statements( content=UPDATED_TTL_DATA, overwrite=True, - graph_uri=None, + graph_iri=None, content_type="application/x-turtle", ) - assert response.status_code == 204 + assert success # fetch the default graph again to ensure data was updated - response, graph = db.fetch_statements( - graph_uri=None, + graph = db.fetch_statements( + graph_iri=None, ) - assert response.status_code == 200 + assert graph is not None assert len(graph) == 7 # now we add data again with overwrite=False - response = db.import_statements( + success = db.import_statements( content=TEST_TTL_DATA, overwrite=False, - graph_uri=None, + graph_iri=None, content_type="application/x-turtle", ) - assert response.status_code == 204 + assert success # fetch the default graph again to ensure data was appended - response, graph = db.fetch_statements( - graph_uri=None, + graph = db.fetch_statements( + graph_iri=None, ) - assert response.status_code == 200 + assert graph is not None assert len(graph) == 9 # clear the default graph - response = db.clear_graph( - graph_uri=None, + success = db.clear_graph( + graph_iri=None, ) - assert response.status_code == 204 + assert success # fetch the default graph again to ensure it is empty - response, graph = db.fetch_statements( - graph_uri=None, + graph = db.fetch_statements( + graph_iri=None, ) - assert response.status_code == 200 + assert graph is not None assert len(graph) == 0 diff --git a/tests/test_triple_get.py b/tests/test_triple_get.py index 51e1d6a..8665092 100644 --- a/tests/test_triple_get.py +++ b/tests/test_triple_get.py @@ -1,18 +1,19 @@ from graph_db_interface import GraphDB from graph_db_interface.utils import utils from graph_db_interface.exceptions import InvalidInputError +from graph_db_interface.utils.iri import IRI from rdflib import Literal, XSD import pytest -SUBJECT1 = "http://example.org/subject1" -PREDICATE1 = "http://example.org/predicate1" -OBJECT1 = Literal(0.5, datatype=XSD.double) # data value +NAMED_GRAPH = "https://my_named_test_graph" -SUBJECT2 = "http://example.org/subject2" -PREDICATE2 = "http://example.org/predicate2" -OBJECT2 = "http://example.org/object2" +SUBJECT1 = "http://example.org#subject1" +PREDICATE1 = "http://example.org#predicate1" +OBJECT1 = 0.5 -NAMED_GRAPH = "" +SUBJECT2 = "http://example.org#subject2" +PREDICATE2 = "http://example.org#predicate2" +OBJECT2 = "http://example.org#object2" @pytest.fixture(params=[None, NAMED_GRAPH], scope="module", autouse=True) @@ -23,14 +24,23 @@ def setup(request, db: GraphDB): # We once set a named graph and once we don't db.named_graph = named_graph - db.triple_add(SUBJECT1, PREDICATE1, OBJECT1) - db.triple_add(SUBJECT2, PREDICATE2, OBJECT2) + db.triples_add( + [ + (SUBJECT1, PREDICATE1, OBJECT1), + (SUBJECT2, PREDICATE2, OBJECT2), + ] + ) yield - db.triple_delete(SUBJECT1, PREDICATE1, OBJECT1, check_exist=True) - db.triple_delete(SUBJECT2, PREDICATE2, OBJECT2, check_exist=True) + db.triples_delete( + [ + (SUBJECT1, PREDICATE1, OBJECT1), + (SUBJECT2, PREDICATE2, OBJECT2), + ] + ) def test_wrong_input(db: GraphDB): + # Neither sub, pred, obj given with pytest.raises(InvalidInputError): db.triples_get() @@ -39,54 +49,36 @@ def test_triple_set_subjects(db: GraphDB): # Unenclosed absolute IRI result_triples = db.triples_get(sub=SUBJECT1, include_implicit=False) result_triples_wrong = db.triples_get(sub=PREDICATE1, include_implicit=False) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1.toPython())] + assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] assert result_triples_wrong == [] # enclosed absolute IRI - result_triples = db.triples_get( - sub=utils.ensure_absolute(SUBJECT1), include_implicit=False - ) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1.toPython())] + result_triples = db.triples_get(sub=SUBJECT1, include_implicit=False) + assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] # shorthand IRI - db.add_prefix("ex", "http://example.org/") + IRI.add_prefix("ex", "http://example.org/") result_triples = db.triples_get( sub=f"ex:{utils.get_local_name(SUBJECT1)}", include_implicit=False ) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1.toPython())] - - # No iri but just a substring of the IRI - result_triples = db.triples_get(sub="example.org/subject", include_implicit=False) - assert sorted(result_triples) == sorted( - [(SUBJECT1, PREDICATE1, OBJECT1.toPython()), (SUBJECT2, PREDICATE2, OBJECT2)] - ) + assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] def test_triple_set_predicates(db: GraphDB): # Unenclosed absolute IRI result_triples = db.triples_get(pred=PREDICATE1, include_implicit=False) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1.toPython())] + assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] # enclosed absolute IRI - result_triples = db.triples_get( - pred=utils.ensure_absolute(PREDICATE1), include_implicit=False - ) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1.toPython())] + result_triples = db.triples_get(pred=f"<{PREDICATE1}>", include_implicit=False) + assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] # shorthand IRI - db.add_prefix("ex", "http://example.org/") + IRI.add_prefix("ex", "http://example.org/") result_triples = db.triples_get( pred=f"ex:{utils.get_local_name(PREDICATE1)}", include_implicit=False ) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1.toPython())] - - # No iri but just a substring of the IRI - result_triples = db.triples_get( - pred="example.org/predicate", include_implicit=False - ) - assert sorted(result_triples) == sorted( - [(SUBJECT1, PREDICATE1, OBJECT1.toPython()), (SUBJECT2, PREDICATE2, OBJECT2)] - ) + assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] def test_triple_set_objects(db: GraphDB): @@ -95,29 +87,23 @@ def test_triple_set_objects(db: GraphDB): assert result_triples == [(SUBJECT2, PREDICATE2, OBJECT2)] # enclosed absolute IRI - result_triples = db.triples_get( - obj=utils.ensure_absolute(OBJECT2), include_implicit=False - ) + result_triples = db.triples_get(obj=f"<{OBJECT2}>", include_implicit=False) assert result_triples == [(SUBJECT2, PREDICATE2, OBJECT2)] # shorthand IRI - db.add_prefix("ex", "http://example.org/") + IRI.add_prefix("ex", "http://example.org/") result_triples = db.triples_get( obj=f"ex:{utils.get_local_name(OBJECT2)}", include_implicit=False ) assert result_triples == [(SUBJECT2, PREDICATE2, OBJECT2)] - # No iri but just a substring of the IRI - result_triples = db.triples_get(obj="example.org/object", include_implicit=False) - assert sorted(result_triples) == sorted([(SUBJECT2, PREDICATE2, OBJECT2)]) - if db.named_graph is not None: - - # Object as a rdflib Literal + # Object as a Python basic type result_triples = db.triples_get(obj=OBJECT1, include_implicit=False) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1.toPython())] + assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] - # Object as a Python basic type - result_triples = db.triples_get(obj=OBJECT1.toPython(), include_implicit=False) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1.toPython())] - pass + # Object as a rdflib Literal + result_triples = db.triples_get( + obj=Literal(OBJECT1, datatype=XSD.double), include_implicit=False + ) + assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] diff --git a/tests/test_utils.py b/tests/test_utils.py index 8b9923f..608b7bc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,6 +4,7 @@ InvalidInputError, InvalidQueryError, ) +from graph_db_interface.utils.iri import IRI import pytest from rdflib import Literal import datetime @@ -30,15 +31,15 @@ def test_validate_query(): def test_validate_update_query(): valid_query = """ DELETE DATA { - GRAPH { - "object" . + GRAPH { + "object" . } } """ invalid_query = """ DELETE DATA { - GRAPH { - + GRAPH { + } } """ @@ -47,26 +48,6 @@ def test_validate_update_query(): utils.validate_update_query(invalid_query) -def test_ensure_absolute(): - iri = "http://www.sfb1574.kit.edu/core" - absolute_iri = utils.ensure_absolute("http://www.sfb1574.kit.edu/core") - assert f"<{iri}>" == absolute_iri - - -def test_is_absolute(): - absolute_iri = "" - relative_iri = "core:Resource" - assert utils.is_absolute(absolute_iri) is True - assert utils.is_absolute(relative_iri) is False - - -def test_strip_angle_brackets(): - iri = "http://www.sfb1574.kit.edu/core" - absolute_iri = f"<{iri}>" - assert iri == utils.strip_angle_brackets(absolute_iri) - assert iri == utils.strip_angle_brackets(iri) - - def test_to_literal(): literal_str = utils.to_literal(42, as_string=True) assert literal_str == '"42"^^' @@ -127,199 +108,41 @@ def test_from_xsd_literal(): def test_convert_query_result_to_python_type(): - result_dict = {"type": "uri", "value": "http://example.org/object2"} + result_dict = {"type": "uri", "value": "http://example.org#object2"} result_dict2 = { "datatype": "http://www.w3.org/2001/XMLSchema#double", "type": "literal", "value": "0.5", } converted_result = utils.convert_query_result_to_python_type(result_dict) - assert converted_result == "http://example.org/object2" + assert converted_result == "http://example.org#object2" converted_result = utils.convert_query_result_to_python_type(result_dict2) assert converted_result == 0.5 def test_get_local_name(): - iri = "" + iri = "" local_name = utils.get_local_name(iri) assert local_name == "subject" - iri = "http://example.org/predicate" + iri = "http://example.org#predicate" local_name = utils.get_local_name(iri) assert local_name == "predicate" - iri = "") is True - assert utils.is_iri("http://example.org/subject#fragment") is True - assert utils.is_iri("http://example.org/subject?query=param") is True - - -def test_is_shorthand_iri(): - assert utils.is_shorthand_iri("ex:subject") is True - assert ( - utils.is_shorthand_iri("ex:subject", prefixes={"ex": "http://example.org/"}) - is True - ) - assert ( - utils.is_shorthand_iri("subject", prefixes={"ex": "http://example.org/"}) - is False - ) - assert utils.is_shorthand_iri("http://example.org/subject") is False - assert utils.is_shorthand_iri("ex:subject", prefixes={}) is True - - -def test_prepare_subject(): - # provide absolute IRI - assert ( - utils.prepare_subject("", ensure_iri=True) - == "" - ) - assert ( - utils.prepare_subject("", ensure_iri=False) - == "" - ) - - # provide IRI, should be turned into absolute IRI - assert ( - utils.prepare_subject("http://example.org/subject", ensure_iri=True) - == "" - ) - assert ( - utils.prepare_subject("http://example.org/subject", ensure_iri=False) - == "" - ) - - # provide shorthand IRI, should be kept as is - assert utils.prepare_subject("ex:subject", ensure_iri=True) == "ex:subject" - assert utils.prepare_subject("ex:subject", ensure_iri=False) == "ex:subject" - - # simple strings should be returned as is - assert utils.prepare_subject("Hello", ensure_iri=False) == "Hello" - with pytest.raises(InvalidIRIError): - utils.prepare_subject("Hello", ensure_iri=True) - - # Literals should not be provided as subjects - with pytest.raises(InvalidInputError): - utils.prepare_subject(Literal("Hello"), ensure_iri=True) - with pytest.raises(InvalidInputError): - utils.prepare_subject(Literal(42.5), ensure_iri=True) - with pytest.raises(InvalidInputError): - utils.prepare_subject(Literal(42.5), ensure_iri=False) - - -def test_prepare_predicate(): - # provide absolute IRI - assert ( - utils.prepare_predicate("", ensure_iri=True) - == "" - ) - assert ( - utils.prepare_predicate("", ensure_iri=False) - == "" - ) - - # provide IRI, should be turned into absolute IRI - assert ( - utils.prepare_predicate("http://example.org/predicate", ensure_iri=True) - == "" - ) - assert ( - utils.prepare_predicate("http://example.org/predicate", ensure_iri=False) - == "" - ) - - # provide shorthand IRI, should be kept as is - assert utils.prepare_predicate("ex:predicate", ensure_iri=True) == "ex:predicate" - assert utils.prepare_predicate("ex:predicate", ensure_iri=False) == "ex:predicate" - - # simple strings should be returned as is - assert utils.prepare_predicate("Hello", ensure_iri=False) == "Hello" - with pytest.raises(InvalidIRIError): - utils.prepare_predicate("Hello", ensure_iri=True) - - # Literals should not be provided as predicates - with pytest.raises(InvalidInputError): - utils.prepare_predicate(Literal("Hello"), ensure_iri=True) - with pytest.raises(InvalidInputError): - utils.prepare_predicate(Literal(42.5), ensure_iri=True) - with pytest.raises(InvalidInputError): - utils.prepare_predicate(Literal(42.5), ensure_iri=False) - - -def test_prepare_object(): - # provide absolute IRI - assert ( - utils.prepare_object("", ensure_iri=True) - == "" - ) - assert ( - utils.prepare_object("", ensure_iri=False) - == "" - ) - - # provide IRI, should be turned into absolute IRI - assert ( - utils.prepare_object("http://example.org/subject", ensure_iri=True) - == "" - ) - assert ( - utils.prepare_object("http://example.org/subject", ensure_iri=False) - == "" - ) - - # provide shorthand IRI, should be kept as is - assert utils.prepare_object("ex:subject", ensure_iri=True) == "ex:subject" - assert utils.prepare_object("ex:subject", ensure_iri=False) == "ex:subject" - - # Literals - assert utils.prepare_object(Literal(42)) == Literal(42) - assert ( - utils.prepare_object(Literal(42), as_string=True) - == '"42"^^' - ) - with pytest.raises(InvalidIRIError): - utils.prepare_object(Literal(42), ensure_iri=True) - - # Simple strings should not be converted since they might be used for filtering - assert utils.prepare_object("Hello", as_string=True) == "Hello" - assert utils.prepare_object("Hello", as_string=False) == "Hello" - with pytest.raises(InvalidIRIError): - utils.prepare_object("Hello", ensure_iri=True) - - # Standard Python types - assert ( - utils.prepare_object(42.5, as_string=True) - == '"42.5"^^' - ) - assert utils.prepare_object(42.5, as_string=False) == Literal(42.5) - with pytest.raises(InvalidIRIError): - utils.prepare_object(42.5, ensure_iri=True) - - def test_encapsulate_named_graph(): - named_graph = "" + named_graph_str = "" + named_graph = IRI(named_graph_str) content = """SELECT * WHERE { ?s ?p ?o . }""" expected_result = f""" -GRAPH {named_graph} {{ +GRAPH {named_graph_str} {{ SELECT * WHERE {{ ?s ?p ?o . From 633264f7814413880b2ee0db700dec96516e0241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 1 Dec 2025 19:15:58 +0100 Subject: [PATCH 03/36] Add new tests - `triple_exists`, `any_triple_exists`, `all_triple_exists`, `sanitize_triple` - `IRI` Class generation, hashing, equality, prefixes, shortening --- tests/test_graph_manipulation.py | 147 ++++++++++++++- tests/test_iri.py | 310 +++++++++++++++++++++++++++++++ tests/test_triple_get.py | 4 + tests/test_utils.py | 152 +++++++++++++++ 4 files changed, 610 insertions(+), 3 deletions(-) create mode 100644 tests/test_iri.py diff --git a/tests/test_graph_manipulation.py b/tests/test_graph_manipulation.py index 2b41a78..82133b9 100644 --- a/tests/test_graph_manipulation.py +++ b/tests/test_graph_manipulation.py @@ -84,14 +84,50 @@ def test_update_triple(db: GraphDB): assert result is True # Input errors + # no new given with pytest.raises(InvalidInputError): db.triple_update((SUBJECT_1, PREDICATE_1, None)) - # Nothing to update + # both new given with pytest.raises(InvalidInputError): - db.triple_update((SUBJECT_1, PREDICATE_1, OBJECT_1)) + db.triple_update( + old_triple=(SUBJECT_1, PREDICATE_1, OBJECT_1), + new_triple=(NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), + new_sub=NEW_SUBJECT_1, + ) + + # old is incomplete + with pytest.raises(InvalidInputError): + db.triple_update( + old_triple=(SUBJECT_1, PREDICATE_1, None), + new_triple=(NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), + ) + + # try to update the full triple + result = db.triple_update( + old_triple=(SUBJECT_1, PREDICATE_1, OBJECT_1), + new_triple=(NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), + ) + assert result is True + + # try to update the individual entries + result = db.triple_update( + old_triple=(NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), new_sub=SUBJECT_1 + ) + assert result is True + + result = db.triple_update( + old_triple=(SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), + new_pred=PREDICATE_1, + ) + assert result is True - # try to update the full triple and change its object + result = db.triple_update( + old_triple=(SUBJECT_1, PREDICATE_1, NEW_OBJECT_1), new_obj=OBJECT_1 + ) + assert result is True + + # adressing via individual arguments result = db.triple_update( (SUBJECT_1, PREDICATE_1, OBJECT_1), new_sub=NEW_SUBJECT_1, @@ -100,6 +136,7 @@ def test_update_triple(db: GraphDB): ) assert result is True + # Cleanup result = db.triple_delete((NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1)) assert result is True @@ -216,6 +253,110 @@ def test_iri_exists(db: GraphDB): assert result is True +def test_triple_exists(db: GraphDB): + # Test return on empty DB + result = db.triple_exists((SUBJECT_1, PREDICATE_1, OBJECT_1)) + assert result is False + + # Add triple + result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) + assert result is True + + # Test if triple is now found + result = db.triple_exists((SUBJECT_1, PREDICATE_1, OBJECT_1)) + assert result is True + + # Test if modified triple is not found + result = db.triple_exists((SUBJECT_2, PREDICATE_1, OBJECT_1)) + assert result is False + + result = db.triple_exists((SUBJECT_1, PREDICATE_2, OBJECT_1)) + assert result is False + + result = db.triple_exists((SUBJECT_1, PREDICATE_1, OBJECT_2)) + assert result is False + + # Cleanup + result = db.triple_delete((SUBJECT_1, PREDICATE_1, OBJECT_1)) + assert result is True + + +def test_multi_triple_exists(db: GraphDB): + # Test return on empty DB + result = db.any_triple_exists( + [ + (SUBJECT_1, PREDICATE_1, OBJECT_1), + (SUBJECT_2, PREDICATE_2, OBJECT_2), + ] + ) + assert result is False + + result = db.all_triple_exists( + [ + (SUBJECT_1, PREDICATE_1, OBJECT_1), + (SUBJECT_2, PREDICATE_2, OBJECT_2), + ] + ) + assert result is False + + # Add first and unrelated triple + result = db.triples_add( + [ + (SUBJECT_1, PREDICATE_1, OBJECT_1), + (NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), + ] + ) + assert result is True + + # One triple now matches + result = db.any_triple_exists( + [ + (SUBJECT_1, PREDICATE_1, OBJECT_1), + (SUBJECT_2, PREDICATE_2, OBJECT_2), + ] + ) + assert result is True + + result = db.all_triple_exists( + [ + (SUBJECT_1, PREDICATE_1, OBJECT_1), + (SUBJECT_2, PREDICATE_2, OBJECT_2), + ] + ) + assert result is False + + # Add second triple + result = db.triple_add((SUBJECT_2, PREDICATE_2, OBJECT_2)) + assert result is True + + # One triple now matches + result = db.any_triple_exists( + [ + (SUBJECT_1, PREDICATE_1, OBJECT_1), + (SUBJECT_2, PREDICATE_2, OBJECT_2), + ] + ) + assert result is True + + result = db.all_triple_exists( + [ + (SUBJECT_1, PREDICATE_1, OBJECT_1), + (SUBJECT_2, PREDICATE_2, OBJECT_2), + ] + ) + assert result is True + + # Cleanup + result = db.triples_delete( + [ + (SUBJECT_1, PREDICATE_1, OBJECT_1), + (SUBJECT_2, PREDICATE_2, OBJECT_2), + (NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), + ] + ) + assert result is True + + def test_convenience_functions(db: GraphDB): subject = "http://example.org#instance" predicate = "rdfs:subClassOf" diff --git a/tests/test_iri.py b/tests/test_iri.py new file mode 100644 index 0000000..8ece1bc --- /dev/null +++ b/tests/test_iri.py @@ -0,0 +1,310 @@ +from graph_db_interface import IRI +from graph_db_interface.exceptions import InvalidIRIError +from rdflib import URIRef, Literal + +import pytest + + +def test_init_formats_onto(): + # Pure onto, without wrappers + ref_iri = IRI("http://www.w3.org/2002/07/owl") + assert ref_iri == "http://www.w3.org/2002/07/owl" + + # Pure onto, with # + iri = IRI("http://www.w3.org/2002/07/owl#") + assert iri == ref_iri + + # Pure onto, with <> + iri = IRI("") + assert iri == ref_iri + + # Pure onto, with <> and # + iri = IRI("") + assert iri == ref_iri + + # Explititly empty name + iri = IRI("", "http://www.w3.org/2002/07/owl") + assert iri == ref_iri + + # Pure onto, duplication + iri = IRI(None, "http://www.w3.org/2002/07/owl") + assert iri == ref_iri + + # Pure onto, using prefix + iri = IRI("owl:") + assert iri == ref_iri + + # Pure onto, malformed IRI + with pytest.raises(InvalidIRIError): + IRI("www.w3.org/2002/07/owl") + + with pytest.raises(InvalidIRIError): + IRI("http://www.w3.org/2002/07:owl#") + + # Pure onto, using unknown prefix + with pytest.raises(InvalidIRIError): + IRI("unknown_prefix:") + + +def test_init_formats_full(): + # Full IRI, without wrappers + ref_iri = IRI("http://www.w3.org/2002/07/owl#Class") + assert ref_iri == "http://www.w3.org/2002/07/owl#Class" + + # Full IRI, with <> + iri = IRI("") + assert iri == ref_iri + + # Full IRI, using prefix + iri = IRI("owl:Class") + assert iri == ref_iri + + # Full IRI, duplication + iri = IRI("http://www.w3.org/2002/07/owl#Class") + iri = IRI(iri) + assert iri == ref_iri + + # Single value, using wrong type + with pytest.raises(TypeError): + IRI(123) + + with pytest.raises(TypeError): + IRI(Literal("http://www.w3.org/2002/07/owl#Class")) + + # Full onto IRI, using ':' instead of '#' + with pytest.raises(InvalidIRIError): + IRI("http://www.w3.org/2002/07/owl:Class") + + # Unknown prefix + with pytest.raises(InvalidIRIError): + IRI("unknown_prefix:Class") + + # Two prefixes + with pytest.raises(InvalidIRIError): + IRI("owl:owl:Class") + + with pytest.raises(InvalidIRIError): + IRI("owl:http://www.w3.org/2002/07/owl#Class") + + with pytest.raises(InvalidIRIError): + IRI("http://www.w3.org/2002/07/owl#owl:Class") + + with pytest.raises(InvalidIRIError): + IRI("http://www.w3.org/2002/07/owl#http://www.w3.org/2002/07/owl#Class") + + with pytest.raises(InvalidIRIError): + IRI("http://www.w3.org/2002/07/owl#www.w3.org/2002/07/owl#Class") + + # mixed : and # + with pytest.raises(InvalidIRIError): + IRI("owl:owl#Class") + + with pytest.raises(InvalidIRIError): + IRI("owl#owl:Class") + + # Prefix, using '#' instead of ':' + with pytest.raises(InvalidIRIError): + IRI("owl#Class") + + +def test_init_formats_two_arguments(): + # Full IRI, without wrappers + ref_iri = IRI("Class", "http://www.w3.org/2002/07/owl") + assert ref_iri == "http://www.w3.org/2002/07/owl#Class" + + # Full IRI, with <> + iri = IRI("Class", "") + assert iri == ref_iri + + # Full IRI, with # + iri = IRI("Class", "http://www.w3.org/2002/07/owl#") + assert iri == ref_iri + + # Full IRI, with # and <> + iri = IRI("Class", "") + assert iri == ref_iri + + # Full IRI, using prefix + iri = IRI("Class", "owl") + assert iri == ref_iri + + # Full IRI, using prefix with : + iri = IRI("Class", "owl:") + assert iri == ref_iri + + iri = IRI("Class", "owl") + iri2 = IRI(iri) + assert iri2 == ref_iri + + # Full iri, using partial iri + iri1 = IRI("http://www.w3.org/2002/07/owl") + iri2 = IRI("Class", iri1) + assert iri2 == ref_iri + + # Transparent cases + iri1 = IRI(ref_iri, None) + assert iri1 == ref_iri + + iri1 = IRI(None, ref_iri) + assert iri1 == ref_iri + + # Two values, wrong type + with pytest.raises(TypeError): + IRI("Class", 123) + + with pytest.raises(TypeError): + IRI(123, "owl:") + + with pytest.raises(TypeError): + IRI("Class", Literal("http://www.w3.org/2002/07/owl")) + + with pytest.raises(TypeError): + IRI(Literal("Class"), "owl:") + + # Empty IRI + with pytest.raises(InvalidIRIError): + IRI("", "") + + with pytest.raises(InvalidIRIError): + IRI(None, None) + + # Unknown prefix + with pytest.raises(InvalidIRIError): + IRI("Class", "abc:") + + # Two prefixes + with pytest.raises(InvalidIRIError): + IRI("Class", "owl:owl:") + + with pytest.raises(InvalidIRIError): + IRI("Class", "http://www.w3.org/2002/07/owl#http://www.w3.org/2002/07/owl#") + + with pytest.raises(InvalidIRIError): + IRI("owl:Class", "owl:") + + with pytest.raises(InvalidIRIError): + IRI("owl:Class", "http://www.w3.org/2002/07/owl#") + + with pytest.raises(InvalidIRIError): + IRI("http://www.w3.org/2002/07/owl#Class", "owl:") + + with pytest.raises(InvalidIRIError): + iri = IRI("Class", "owl") + IRI(iri, "owl") + + +def test_prefix_management(): + """Test add_prefix, remove_prefix, and get_prefixes functionality""" + + # Get initial prefixes (should include default prefixes: owl, rdf, rdfs, onto) + initial_prefixes = IRI.get_prefixes() + assert isinstance(initial_prefixes, dict) + assert all( + prefix in initial_prefixes + for prefix in [ + "owl", + "rdf", + "rdfs", + "kafka", + "kafka-inst", + ] + ) + initial_count = len(initial_prefixes) + + # Test add_prefix + IRI.add_prefix("ex", "http://example.org#") + prefixes = IRI.get_prefixes() + assert "ex" in prefixes + assert prefixes["ex"] == "http://example.org" + assert len(prefixes) == initial_count + 1 + + # Test add_prefix with IRI with angle brackets (should remove them) + IRI.add_prefix("test", "") + prefixes = IRI.get_prefixes() + assert "test" in prefixes + assert prefixes["test"] == "http://test.org" + assert len(prefixes) == initial_count + 2 + + # Test overwriting an existing prefix + IRI.add_prefix("ex", "http://example.com") + prefixes = IRI.get_prefixes() + assert prefixes["ex"] == "http://example.com" + assert len(prefixes) == initial_count + 2 # Count shouldn't increase + + # Test remove_prefix for existing prefix + result = IRI.remove_prefix("ex") + assert result is True + prefixes = IRI.get_prefixes() + assert "ex" not in prefixes + assert len(prefixes) == initial_count + 1 + + # Test remove_prefix for non-existing prefix + result = IRI.remove_prefix("nonexistent") + assert result is False + + # Test remove_prefix for another existing prefix + result = IRI.remove_prefix("test") + assert result is True + prefixes = IRI.get_prefixes() + assert "test" not in prefixes + assert len(prefixes) == initial_count + + # Verify default prefixes are still intact + assert all( + prefix in initial_prefixes + for prefix in [ + "owl", + "rdf", + "rdfs", + "kafka", + "kafka-inst", + ] + ) + + +def test_eq(): + iri_ref = IRI("http://example.org#Test") + + iri2 = IRI("http://example.org#Test") + assert iri_ref == iri2 + + iri3 = IRI("http://example.org#AnotherTest") + assert iri_ref != iri3 + + str_iri = "http://example.org#Test" + assert iri_ref == str_iri + + str_iri_different = "http://example.org#Different" + assert iri_ref != str_iri_different + + str_not_iri = "NotAnIRI" + assert iri_ref != str_not_iri + + literal_iri = Literal("http://example.org#Test") + assert iri_ref != literal_iri + + uri_ref_iri = URIRef("http://example.org#Test") + assert iri_ref == uri_ref_iri + + uri_ref_iri_different = URIRef("http://example.org#Different") + assert iri_ref != uri_ref_iri_different + + not_a_string_or_iri = 12345 + assert iri_ref != not_a_string_or_iri + + +def test_hash(): + iri1 = IRI("http://example.org#Test") + iri2 = IRI("http://example.org#Test") + iri3 = IRI("http://example.org#AnotherTest") + + assert hash(iri1) == hash(iri2) + assert hash(iri1) != hash(iri3) + + +def test_short(): + iri1 = IRI("http://www.w3.org/2002/07/owl#Class") + iri3 = IRI("http://unknown.org#Entity") + + assert iri1.short() == "owl:Class" + assert iri3.short() == "http://unknown.org#Entity" diff --git a/tests/test_triple_get.py b/tests/test_triple_get.py index 8665092..092c8a5 100644 --- a/tests/test_triple_get.py +++ b/tests/test_triple_get.py @@ -44,6 +44,10 @@ def test_wrong_input(db: GraphDB): with pytest.raises(InvalidInputError): db.triples_get() + # Both triple and explicit iri given + with pytest.raises(InvalidInputError): + db.triples_get((SUBJECT1, PREDICATE1, OBJECT1), sub=SUBJECT1) + def test_triple_set_subjects(db: GraphDB): # Unenclosed absolute IRI diff --git a/tests/test_utils.py b/tests/test_utils.py index 608b7bc..dda30e5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,6 +10,158 @@ import datetime +def test_sanitize_triple(): + triple_three_iri = ( + IRI("http://example.org#subject"), + IRI("http://example.org#predicate"), + IRI("http://example.org#object"), + ) + # Must not modify + sanitized_triple = utils.sanitize_triple(triple_three_iri) + assert sanitized_triple == triple_three_iri + + triple = ( + "http://example.org#subject", + "http://example.org#predicate", + "http://example.org#object", + ) + # Must convert first two str to IRI, last to Literal + sanitized_triple = utils.sanitize_triple(triple) + assert sanitized_triple == triple_three_iri + + triple_iri_literal_string = ( + IRI("http://example.org#subject"), + IRI("http://example.org#predicate"), + Literal("literal_object"), + ) + # Must not modify + sanitized_triple = utils.sanitize_triple(triple_iri_literal_string) + assert sanitized_triple == triple_iri_literal_string + + triple_iri_literal_number = ( + IRI("http://example.org#subject"), + IRI("http://example.org#predicate"), + Literal(2.0), + ) + # Must not modify reference triple + sanitized_triple = utils.sanitize_triple(triple_iri_literal_number) + assert sanitized_triple == triple_iri_literal_number + + triple = ( + IRI("http://example.org#subject"), + IRI("http://example.org#predicate"), + 2.0, + ) + # Must convert python type to Literal + sanitized_triple = utils.sanitize_triple(triple) + assert sanitized_triple == triple_iri_literal_number + + triple = ( + "http://example.org#subject", + "http://example.org#predicate", + "literal_object", + ) + # Object str cannot be converted to IRI + with pytest.raises(InvalidIRIError): + utils.sanitize_triple(triple) + + triple = ( + IRI("http://example.org#subject"), + "not_an_iri", + IRI("http://example.org#object"), + ) + # second element not convertible to IRI + with pytest.raises(InvalidIRIError): + utils.sanitize_triple(triple) + + triple = ( + IRI("http://example.org#subject"), + IRI("http://example.org#predicate"), + ) + # only 2 elements + with pytest.raises(InvalidInputError): + utils.sanitize_triple(triple) + + triple = ( + Literal("http://example.org#subject"), + Literal("http://example.org#predicate"), + IRI("http://example.org#object"), + ) + # first two elements are Literals, must be IRIs + with pytest.raises(TypeError): + utils.sanitize_triple(triple) + + +def test_sanitize_triple_partial(): + partial_triple_two_iri = ( + IRI("http://example.org#subject"), + None, + IRI("http://example.org#object"), + ) + # Must not modify + sanitized_triple = utils.sanitize_triple(partial_triple_two_iri, allow_partial=True) + assert sanitized_triple == partial_triple_two_iri + + partial_triple_mixed = ( + "http://example.org#subject", + None, + IRI("http://example.org#object"), + ) + # Must convert second str to IRI + sanitized_triple = utils.sanitize_triple(partial_triple_mixed, allow_partial=True) + assert sanitized_triple == partial_triple_two_iri + + partial_triple_iri_literal = ( + IRI("http://example.org#subject"), + None, + Literal("literal_string"), + ) + # Must not modify + sanitized_triple = utils.sanitize_triple( + partial_triple_iri_literal, allow_partial=True + ) + assert sanitized_triple == partial_triple_iri_literal + + partial_triple_iri_literal_number = ( + IRI("http://example.org#subject"), + None, + Literal(2.0), + ) + # Must not modify + sanitized_triple = utils.sanitize_triple( + partial_triple_iri_literal_number, allow_partial=True + ) + assert sanitized_triple == partial_triple_iri_literal_number + + partial_triple_iri_number = ( + IRI("http://example.org#subject"), + None, + 2.0, + ) + # Must convert python type to Literal + sanitized_triple = utils.sanitize_triple( + partial_triple_iri_number, allow_partial=True + ) + assert sanitized_triple == partial_triple_iri_literal_number + + partial_triple_mixed = ( + IRI("http://example.org#subject"), + None, + "literal_string", + ) + # Object str cannot be converted to IRI + with pytest.raises(InvalidIRIError): + utils.sanitize_triple(partial_triple_mixed, allow_partial=True) + + partial_triple_invalid = ( + IRI("http://example.org#subject"), + Literal("http://example.org#predicate"), + None, + ) + with pytest.raises(TypeError): + utils.sanitize_triple(partial_triple_invalid, allow_partial=True) + + def test_validate_query(): valid_query = """ SELECT * From eafaf6b677663ebf81ef8ea5fb07fc48d7dc0f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 1 Dec 2025 20:09:50 +0100 Subject: [PATCH 04/36] Update test naming convention --- tests/test_graph_manipulation.py | 265 +++++++++++++++++-------------- tests/test_triple_get.py | 68 ++++---- 2 files changed, 178 insertions(+), 155 deletions(-) diff --git a/tests/test_graph_manipulation.py b/tests/test_graph_manipulation.py index 82133b9..ba7052e 100644 --- a/tests/test_graph_manipulation.py +++ b/tests/test_graph_manipulation.py @@ -1,4 +1,3 @@ -from typing import Tuple, List, Union import pytest from rdflib import Literal, XSD from graph_db_interface import GraphDB @@ -7,32 +6,21 @@ NAMED_GRAPH = "http://example.org/named_graph" -SUBJECT_1 = "http://example.org#subject_1" -PREDICATE_1 = "http://example.org#predicate_1" -OBJECT_1 = 0.5 +SUB_1 = "http://example.org#subject_1" +PRED_1 = "http://example.org#predicate_1" +OBJ_1 = 0.5 -SUBJECT_2 = "http://example.org#subject_2" -PREDICATE_2 = "http://example.org#predicate_2" -OBJECT_2 = 42 +SUB_2 = "http://example.org#subject_2" +PRED_2 = "http://example.org#predicate_2" +OBJ_2 = 42 +NEW_SUB_1 = "http://example.org#new_subject" +NEW_PRED_1 = "http://example.org#new_predicate" +NEW_OBJ_1 = Literal('string with "quotes"', datatype=XSD.string) -NEW_SUBJECT_1 = "http://example.org#new_subject" -NEW_PREDICATE_1 = "http://example.org#new_predicate" -NEW_OBJECT_1 = Literal('string with "quotes"', datatype=XSD.string) - -NEW_SUBJECT_2 = "http://example.org#new_subject_2" -NEW_PREDICATE_2 = "http://example.org#new_predicate_2" -NEW_OBJECT_2 = True - - -LIST_OF_TRIPLES: List[Tuple[str, str, Union[str, Literal]]] = [ - ((SUBJECT_1, PREDICATE_1, OBJECT_1)), - (SUBJECT_2, PREDICATE_2, OBJECT_2), -] -LIST_OF_NEW_TRIPLES: List[Tuple[str, str, Union[str, Literal]]] = [ - (NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), - (NEW_SUBJECT_2, NEW_PREDICATE_2, NEW_OBJECT_2), -] +NEW_SUB_2 = "http://example.org#new_subject_2" +NEW_PRED_2 = "http://example.org#new_predicate_2" +NEW_OBJ_2 = True @pytest.fixture(params=[None, NAMED_GRAPH], scope="module", autouse=True) @@ -44,197 +32,232 @@ def setup(request, db: GraphDB): def test_add_and_delete_triple(db: GraphDB): # Add a new triple - result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_add((SUB_1, PRED_1, OBJ_1)) assert result is True # try to delete the triple - result = db.triple_delete((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_delete((SUB_1, PRED_1, OBJ_1)) assert result is True # try to delete the triple again - result = db.triple_delete((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_delete((SUB_1, PRED_1, OBJ_1)) assert result is False # if we dont check for existence it shoud return True - result = db.triple_delete((SUBJECT_1, PREDICATE_1, OBJECT_1), check_exist=False) + result = db.triple_delete((SUB_1, PRED_1, OBJ_1), check_exist=False) assert result is True def test_add_and_delete_multiple_triples(db: GraphDB): # add multiple triples - result = db.triples_add(LIST_OF_TRIPLES) + result = db.triples_add( + [ + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), + ] + ) assert result is True # try to delete the triples - result = db.triples_delete(LIST_OF_TRIPLES) + result = db.triples_delete( + [ + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), + ] + ) assert result is True # try to delete the triples again - result = db.triples_delete(LIST_OF_TRIPLES) + result = db.triples_delete( + [ + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), + ] + ) assert result is False # if we dont check for existence it shoud return True - result = db.triples_delete(LIST_OF_TRIPLES, check_exist=False) + result = db.triples_delete( + [ + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), + ], + check_exist=False, + ) assert result is True def test_update_triple(db: GraphDB): # Add a new triple - result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_add((SUB_1, PRED_1, OBJ_1)) assert result is True # Input errors # no new given with pytest.raises(InvalidInputError): - db.triple_update((SUBJECT_1, PREDICATE_1, None)) + db.triple_update(old_triple=(SUB_1, PRED_1, OBJ_1)) # both new given with pytest.raises(InvalidInputError): db.triple_update( - old_triple=(SUBJECT_1, PREDICATE_1, OBJECT_1), - new_triple=(NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), - new_sub=NEW_SUBJECT_1, + old_triple=(SUB_1, PRED_1, OBJ_1), + new_triple=(NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), + new_sub=NEW_SUB_1, ) # old is incomplete with pytest.raises(InvalidInputError): db.triple_update( - old_triple=(SUBJECT_1, PREDICATE_1, None), - new_triple=(NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), + old_triple=(SUB_1, PRED_1, None), + new_triple=(NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), ) # try to update the full triple result = db.triple_update( - old_triple=(SUBJECT_1, PREDICATE_1, OBJECT_1), - new_triple=(NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), + old_triple=(SUB_1, PRED_1, OBJ_1), + new_triple=(NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), ) assert result is True # try to update the individual entries result = db.triple_update( - old_triple=(NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), new_sub=SUBJECT_1 + old_triple=(NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), new_sub=SUB_1 ) assert result is True result = db.triple_update( - old_triple=(SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), - new_pred=PREDICATE_1, + old_triple=(SUB_1, NEW_PRED_1, NEW_OBJ_1), + new_pred=PRED_1, ) assert result is True - result = db.triple_update( - old_triple=(SUBJECT_1, PREDICATE_1, NEW_OBJECT_1), new_obj=OBJECT_1 - ) + result = db.triple_update(old_triple=(SUB_1, PRED_1, NEW_OBJ_1), new_obj=OBJ_1) assert result is True # adressing via individual arguments result = db.triple_update( - (SUBJECT_1, PREDICATE_1, OBJECT_1), - new_sub=NEW_SUBJECT_1, - new_pred=NEW_PREDICATE_1, - new_obj=NEW_OBJECT_1, + (SUB_1, PRED_1, OBJ_1), + new_sub=NEW_SUB_1, + new_pred=NEW_PRED_1, + new_obj=NEW_OBJ_1, ) assert result is True # Cleanup - result = db.triple_delete((NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1)) + result = db.triple_delete((NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1)) assert result is True def test_update_triple_only_subject(db: GraphDB): # Add a new triple - result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_add((SUB_1, PRED_1, OBJ_1)) assert result is True # only update the subject of the triple result = db.triple_update( - (SUBJECT_1, PREDICATE_1, OBJECT_1), - new_sub=NEW_SUBJECT_1, + old_triple=(SUB_1, PRED_1, OBJ_1), + new_triple=(NEW_SUB_1, None, None), ) assert result is True # try to delete the triple - result = db.triple_delete((NEW_SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_delete((NEW_SUB_1, PRED_1, OBJ_1)) assert result is True def test_update_triple_only_predicate(db: GraphDB): # Add a new triple - result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_add((SUB_1, PRED_1, OBJ_1)) assert result is True # only update the predicate of the triple result = db.triple_update( - (SUBJECT_1, PREDICATE_1, OBJECT_1), - new_pred=NEW_PREDICATE_1, + old_triple=(SUB_1, PRED_1, OBJ_1), + new_triple=(None, NEW_PRED_1, None), ) assert result is True # try to delete the triple - result = db.triple_delete((SUBJECT_1, NEW_PREDICATE_1, OBJECT_1)) + result = db.triple_delete((SUB_1, NEW_PRED_1, OBJ_1)) assert result is True def test_update_triple_only_object(db: GraphDB): # Add a new triple - result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_add((SUB_1, PRED_1, OBJ_1)) assert result is True # only update the object of the triple result = db.triple_update( - (SUBJECT_1, PREDICATE_1, OBJECT_1), - new_obj=NEW_OBJECT_1, + old_triple=(SUB_1, PRED_1, OBJ_1), + new_triple=(None, None, NEW_OBJ_1), ) assert result is True # try to delete the triple - result = db.triple_delete((SUBJECT_1, PREDICATE_1, NEW_OBJECT_1)) + result = db.triple_delete((SUB_1, PRED_1, NEW_OBJ_1)) assert result is True def test_update_multiple_triples(db: GraphDB): # add multiple triples - result = db.triples_add(LIST_OF_TRIPLES) + result = db.triples_add( + [ + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), + ] + ) assert result is True # update multiple triples result = db.triples_update( - old_triples=LIST_OF_TRIPLES, - new_triples=LIST_OF_NEW_TRIPLES, + old_triples=[ + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), + ], + new_triples=[ + (NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), + (NEW_SUB_2, NEW_PRED_2, NEW_OBJ_2), + ], ) assert result is True # try to delete the new triples - result = db.triples_delete(LIST_OF_NEW_TRIPLES) + result = db.triples_delete( + [ + (NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), + (NEW_SUB_2, NEW_PRED_2, NEW_OBJ_2), + ] + ) assert result is True def test_iri_exists(db: GraphDB): # add a new triple to the default graph - result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_add((SUB_1, PRED_1, OBJ_1)) assert result is True # does not specify any part of a triple to look for with pytest.raises(InvalidInputError): - db.iri_exists(iri=SUBJECT_1) + db.iri_exists(iri=SUB_1) # IRI should exist like this result = db.iri_exists( - iri=SUBJECT_1, + iri=SUB_1, as_sub=True, include_explicit=True, include_implicit=False, ) assert result is True - result = db.iri_exists(SUBJECT_1, as_sub=True, as_pred=True) + result = db.iri_exists(SUB_1, as_sub=True, as_pred=True) assert result is False - result = db.iri_exists(PREDICATE_1, as_pred=True) + result = db.iri_exists(PRED_1, as_pred=True) assert result is True result = db.iri_exists( - SUBJECT_1, + SUB_1, as_obj=True, include_explicit=True, include_implicit=False, @@ -242,42 +265,42 @@ def test_iri_exists(db: GraphDB): assert result is False result = db.iri_exists( - SUBJECT_1, + SUB_1, as_pred=True, include_explicit=True, include_implicit=False, ) assert result is False - result = db.triple_delete((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_delete((SUB_1, PRED_1, OBJ_1)) assert result is True def test_triple_exists(db: GraphDB): # Test return on empty DB - result = db.triple_exists((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_exists((SUB_1, PRED_1, OBJ_1)) assert result is False # Add triple - result = db.triple_add((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_add((SUB_1, PRED_1, OBJ_1)) assert result is True # Test if triple is now found - result = db.triple_exists((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_exists((SUB_1, PRED_1, OBJ_1)) assert result is True # Test if modified triple is not found - result = db.triple_exists((SUBJECT_2, PREDICATE_1, OBJECT_1)) + result = db.triple_exists((SUB_2, PRED_1, OBJ_1)) assert result is False - result = db.triple_exists((SUBJECT_1, PREDICATE_2, OBJECT_1)) + result = db.triple_exists((SUB_1, PRED_2, OBJ_1)) assert result is False - result = db.triple_exists((SUBJECT_1, PREDICATE_1, OBJECT_2)) + result = db.triple_exists((SUB_1, PRED_1, OBJ_2)) assert result is False # Cleanup - result = db.triple_delete((SUBJECT_1, PREDICATE_1, OBJECT_1)) + result = db.triple_delete((SUB_1, PRED_1, OBJ_1)) assert result is True @@ -285,16 +308,16 @@ def test_multi_triple_exists(db: GraphDB): # Test return on empty DB result = db.any_triple_exists( [ - (SUBJECT_1, PREDICATE_1, OBJECT_1), - (SUBJECT_2, PREDICATE_2, OBJECT_2), + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), ] ) assert result is False result = db.all_triple_exists( [ - (SUBJECT_1, PREDICATE_1, OBJECT_1), - (SUBJECT_2, PREDICATE_2, OBJECT_2), + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), ] ) assert result is False @@ -302,8 +325,8 @@ def test_multi_triple_exists(db: GraphDB): # Add first and unrelated triple result = db.triples_add( [ - (SUBJECT_1, PREDICATE_1, OBJECT_1), - (NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), + (SUB_1, PRED_1, OBJ_1), + (NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), ] ) assert result is True @@ -311,37 +334,37 @@ def test_multi_triple_exists(db: GraphDB): # One triple now matches result = db.any_triple_exists( [ - (SUBJECT_1, PREDICATE_1, OBJECT_1), - (SUBJECT_2, PREDICATE_2, OBJECT_2), + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), ] ) assert result is True result = db.all_triple_exists( [ - (SUBJECT_1, PREDICATE_1, OBJECT_1), - (SUBJECT_2, PREDICATE_2, OBJECT_2), + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), ] ) assert result is False # Add second triple - result = db.triple_add((SUBJECT_2, PREDICATE_2, OBJECT_2)) + result = db.triple_add((SUB_2, PRED_2, OBJ_2)) assert result is True # One triple now matches result = db.any_triple_exists( [ - (SUBJECT_1, PREDICATE_1, OBJECT_1), - (SUBJECT_2, PREDICATE_2, OBJECT_2), + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), ] ) assert result is True result = db.all_triple_exists( [ - (SUBJECT_1, PREDICATE_1, OBJECT_1), - (SUBJECT_2, PREDICATE_2, OBJECT_2), + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), ] ) assert result is True @@ -349,60 +372,60 @@ def test_multi_triple_exists(db: GraphDB): # Cleanup result = db.triples_delete( [ - (SUBJECT_1, PREDICATE_1, OBJECT_1), - (SUBJECT_2, PREDICATE_2, OBJECT_2), - (NEW_SUBJECT_1, NEW_PREDICATE_1, NEW_OBJECT_1), + (SUB_1, PRED_1, OBJ_1), + (SUB_2, PRED_2, OBJ_2), + (NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), ] ) assert result is True def test_convenience_functions(db: GraphDB): - subject = "http://example.org#instance" - predicate = "rdfs:subClassOf" - object = "http://example.org#myClass" + sub = "http://example.org#instance" + pred = "rdfs:subClassOf" + obj = "http://example.org#myClass" - result = db.triple_add((subject, predicate, object)) + result = db.triple_add((sub, pred, obj)) assert result is True - result = db.triple_add((subject, "rdf:type", "owl:NamedIndividual")) + result = db.triple_add((sub, "rdf:type", "owl:NamedIndividual")) assert result is True - result = db.triple_add((subject, "rdf:type", object)) + result = db.triple_add((sub, "rdf:type", obj)) assert result is True - result = db.triple_add((object, "rdf:type", "owl:Class")) + result = db.triple_add((obj, "rdf:type", "owl:Class")) assert result is True - result = db.is_subclass(subject, object) + result = db.is_subclass(sub, obj) assert result is True - result = db.is_subclass(subject, "http://example.org#someNonExistingClass") + result = db.is_subclass(sub, "http://example.org#someNonExistingClass") assert result is False - result = db.owl_is_named_individual(subject) + result = db.owl_is_named_individual(sub) assert result is True - result = db.owl_is_named_individual(predicate) + result = db.owl_is_named_individual(pred) assert result is False - classes = db.owl_get_classes_of_individual(subject, local_name=False) - assert classes == [object] + classes = db.owl_get_classes_of_individual(sub, local_name=False) + assert classes == [obj] - classes = db.owl_get_classes_of_individual(subject, local_name=True) - assert classes == [utils.get_local_name(object)] + classes = db.owl_get_classes_of_individual(sub, local_name=True) + assert classes == [utils.get_local_name(obj)] - classes = db.owl_get_classes_of_individual(object, local_name=False) + classes = db.owl_get_classes_of_individual(obj, local_name=False) assert classes == [] - result = db.triple_delete((subject, predicate, object)) + result = db.triple_delete((sub, pred, obj)) assert result is True - result = db.triple_delete((subject, "rdf:type", "owl:NamedIndividual")) + result = db.triple_delete((sub, "rdf:type", "owl:NamedIndividual")) assert result is True - result = db.triple_delete((subject, "rdf:type", object)) + result = db.triple_delete((sub, "rdf:type", obj)) assert result is True - result = db.triple_delete((object, "rdf:type", "owl:Class")) + result = db.triple_delete((obj, "rdf:type", "owl:Class")) assert result is True diff --git a/tests/test_triple_get.py b/tests/test_triple_get.py index 092c8a5..c7065d5 100644 --- a/tests/test_triple_get.py +++ b/tests/test_triple_get.py @@ -7,13 +7,13 @@ NAMED_GRAPH = "https://my_named_test_graph" -SUBJECT1 = "http://example.org#subject1" -PREDICATE1 = "http://example.org#predicate1" -OBJECT1 = 0.5 +SUB_1 = "http://example.org#subject1" +PRED_1 = "http://example.org#predicate1" +OBJ_1 = 0.5 -SUBJECT2 = "http://example.org#subject2" -PREDICATE2 = "http://example.org#predicate2" -OBJECT2 = "http://example.org#object2" +SUBJ_2 = "http://example.org#subject2" +PRED_2 = "http://example.org#predicate2" +OBJ_2 = "http://example.org#object2" @pytest.fixture(params=[None, NAMED_GRAPH], scope="module", autouse=True) @@ -26,15 +26,15 @@ def setup(request, db: GraphDB): db.triples_add( [ - (SUBJECT1, PREDICATE1, OBJECT1), - (SUBJECT2, PREDICATE2, OBJECT2), + (SUB_1, PRED_1, OBJ_1), + (SUBJ_2, PRED_2, OBJ_2), ] ) yield db.triples_delete( [ - (SUBJECT1, PREDICATE1, OBJECT1), - (SUBJECT2, PREDICATE2, OBJECT2), + (SUB_1, PRED_1, OBJ_1), + (SUBJ_2, PRED_2, OBJ_2), ] ) @@ -46,68 +46,68 @@ def test_wrong_input(db: GraphDB): # Both triple and explicit iri given with pytest.raises(InvalidInputError): - db.triples_get((SUBJECT1, PREDICATE1, OBJECT1), sub=SUBJECT1) + db.triples_get((SUB_1, PRED_1, OBJ_1), sub=SUB_1) def test_triple_set_subjects(db: GraphDB): # Unenclosed absolute IRI - result_triples = db.triples_get(sub=SUBJECT1, include_implicit=False) - result_triples_wrong = db.triples_get(sub=PREDICATE1, include_implicit=False) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] + result_triples = db.triples_get(sub=SUB_1, include_implicit=False) + result_triples_wrong = db.triples_get(sub=PRED_1, include_implicit=False) + assert result_triples == [(SUB_1, PRED_1, OBJ_1)] assert result_triples_wrong == [] # enclosed absolute IRI - result_triples = db.triples_get(sub=SUBJECT1, include_implicit=False) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] + result_triples = db.triples_get(sub=SUB_1, include_implicit=False) + assert result_triples == [(SUB_1, PRED_1, OBJ_1)] # shorthand IRI IRI.add_prefix("ex", "http://example.org/") result_triples = db.triples_get( - sub=f"ex:{utils.get_local_name(SUBJECT1)}", include_implicit=False + sub=f"ex:{utils.get_local_name(SUB_1)}", include_implicit=False ) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] + assert result_triples == [(SUB_1, PRED_1, OBJ_1)] def test_triple_set_predicates(db: GraphDB): # Unenclosed absolute IRI - result_triples = db.triples_get(pred=PREDICATE1, include_implicit=False) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] + result_triples = db.triples_get(pred=PRED_1, include_implicit=False) + assert result_triples == [(SUB_1, PRED_1, OBJ_1)] # enclosed absolute IRI - result_triples = db.triples_get(pred=f"<{PREDICATE1}>", include_implicit=False) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] + result_triples = db.triples_get(pred=f"<{PRED_1}>", include_implicit=False) + assert result_triples == [(SUB_1, PRED_1, OBJ_1)] # shorthand IRI IRI.add_prefix("ex", "http://example.org/") result_triples = db.triples_get( - pred=f"ex:{utils.get_local_name(PREDICATE1)}", include_implicit=False + pred=f"ex:{utils.get_local_name(PRED_1)}", include_implicit=False ) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] + assert result_triples == [(SUB_1, PRED_1, OBJ_1)] def test_triple_set_objects(db: GraphDB): # Unenclosed absolute IRI - result_triples = db.triples_get(obj=OBJECT2, include_implicit=False) - assert result_triples == [(SUBJECT2, PREDICATE2, OBJECT2)] + result_triples = db.triples_get(obj=OBJ_2, include_implicit=False) + assert result_triples == [(SUBJ_2, PRED_2, OBJ_2)] # enclosed absolute IRI - result_triples = db.triples_get(obj=f"<{OBJECT2}>", include_implicit=False) - assert result_triples == [(SUBJECT2, PREDICATE2, OBJECT2)] + result_triples = db.triples_get(obj=f"<{OBJ_2}>", include_implicit=False) + assert result_triples == [(SUBJ_2, PRED_2, OBJ_2)] # shorthand IRI IRI.add_prefix("ex", "http://example.org/") result_triples = db.triples_get( - obj=f"ex:{utils.get_local_name(OBJECT2)}", include_implicit=False + obj=f"ex:{utils.get_local_name(OBJ_2)}", include_implicit=False ) - assert result_triples == [(SUBJECT2, PREDICATE2, OBJECT2)] + assert result_triples == [(SUBJ_2, PRED_2, OBJ_2)] if db.named_graph is not None: # Object as a Python basic type - result_triples = db.triples_get(obj=OBJECT1, include_implicit=False) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] + result_triples = db.triples_get(obj=OBJ_1, include_implicit=False) + assert result_triples == [(SUB_1, PRED_1, OBJ_1)] # Object as a rdflib Literal result_triples = db.triples_get( - obj=Literal(OBJECT1, datatype=XSD.double), include_implicit=False + obj=Literal(OBJ_1, datatype=XSD.double), include_implicit=False ) - assert result_triples == [(SUBJECT1, PREDICATE1, OBJECT1)] + assert result_triples == [(SUB_1, PRED_1, OBJ_1)] From fa2b9d1531d8179ac6f7b426fdd042e50fff9cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 1 Dec 2025 20:10:05 +0100 Subject: [PATCH 05/36] Squash: Implement changes, update docstrings --- graph_db_interface/__init__.py | 2 + graph_db_interface/exceptions.py | 79 +++- graph_db_interface/graph_db.py | 210 ++++++--- graph_db_interface/kafka/kafka_manager.py | 108 ++--- graph_db_interface/queries/named_graph.py | 21 +- .../queries/ontology_helpers.py | 147 +++--- .../queries/rdf4j/graph_store.py | 199 ++++---- graph_db_interface/queries/triple_multi.py | 425 ++++++++++-------- graph_db_interface/queries/triple_single.py | 269 +++++------ graph_db_interface/sparql_query.py | 169 +++++-- .../utils/graph_db_credentials.py | 42 +- graph_db_interface/utils/iri.py | 292 ++++++++++++ graph_db_interface/utils/pretty_print.py | 88 ++-- graph_db_interface/utils/processing.py | 82 +--- graph_db_interface/utils/utils.py | 407 ++++++++--------- 15 files changed, 1506 insertions(+), 1034 deletions(-) create mode 100644 graph_db_interface/utils/iri.py diff --git a/graph_db_interface/__init__.py b/graph_db_interface/__init__.py index c44e6fa..eb9c735 100644 --- a/graph_db_interface/__init__.py +++ b/graph_db_interface/__init__.py @@ -1,6 +1,7 @@ from .sparql_query import SPARQLQuery from .graph_db import GraphDB from .utils.graph_db_credentials import GraphDBCredentials +from .utils.iri import IRI from .utils.utils import to_literal from .utils.processing import process_bindings_select from .utils.pretty_print import format_result @@ -9,6 +10,7 @@ __all__ = [ "GraphDB", "GraphDBCredentials", + "IRI", "SPARQLQuery", "to_literal", "process_bindings_select", diff --git a/graph_db_interface/exceptions.py b/graph_db_interface/exceptions.py index 535ef0f..2f3cfa8 100644 --- a/graph_db_interface/exceptions.py +++ b/graph_db_interface/exceptions.py @@ -1,52 +1,105 @@ class GraphDBInterfaceError(Exception): - """Base class for exceptions in this module.""" + """ + Base exception for the GraphDB interface. + + Serves as the common ancestor for all custom exceptions in this + package to allow catching them collectively when desired. + """ pass class InvalidRepositoryError(GraphDBInterfaceError): - """Exception raised for invalid repository.""" + """ + Invalid repository configuration or selection. + + Args: + message (str): Explanation of why the repository is invalid. + """ - def __init__(self, message: str): + def __init__( + self, + message: str, + ): self.message = message super().__init__(self.message) class AuthenticationError(GraphDBInterfaceError): - """Exception raised for authentication errors.""" + """ + Authentication failure when communicating with GraphDB. - def __init__(self, message: str): + Args: + message (str): Details about the authentication error. + """ + + def __init__( + self, + message: str, + ): self.message = message super().__init__(self.message) class InvalidQueryError(GraphDBInterfaceError): - """Exception raised for invalid SPARQL queries.""" + """ + Invalid SPARQL query or update string. + + Args: + message (str): Description of why the query is invalid. + """ - def __init__(self, message: str): + def __init__( + self, + message: str, + ): self.message = message super().__init__(self.message) class InvalidInputError(GraphDBInterfaceError): - """Exception raised for invalid input.""" + """ + Invalid input provided to an interface method. - def __init__(self, message: str): + Args: + message (str): Explanation of the invalid input condition. + """ + + def __init__( + self, + message: str, + ): self.message = message super().__init__(self.message) class InvalidIRIError(GraphDBInterfaceError): - """Exception raised for invalid IRIs.""" + """ + Invalid IRI value or format encountered. + + Args: + message (str): Description of the IRI validation failure. + """ - def __init__(self, message: str): + def __init__( + self, + message: str, + ): self.message = message super().__init__(self.message) class GraphDbException(GraphDBInterfaceError): - """Exception raised for general GraphDB errors.""" + """ + General error raised for HTTP or runtime issues with GraphDB. + + Args: + message (str): The error message returned or constructed for the failure. + """ - def __init__(self, message: str): + def __init__( + self, + message: str, + ): self.message = message super().__init__(self.message) diff --git a/graph_db_interface/graph_db.py b/graph_db_interface/graph_db.py index 5f6f549..bc528f7 100644 --- a/graph_db_interface/graph_db.py +++ b/graph_db_interface/graph_db.py @@ -1,12 +1,13 @@ +from __future__ import annotations + from base64 import b64encode from typing import List, Union, Optional, Dict from graph_db_interface.kafka.kafka_manager import KafkaManager import requests import logging -import os from requests import Response -from graph_db_interface.utils import utils from graph_db_interface.utils.graph_db_credentials import GraphDBCredentials +from graph_db_interface.utils.iri import IRI from graph_db_interface.exceptions import ( InvalidRepositoryError, AuthenticationError, @@ -15,14 +16,20 @@ class GraphDB: - """A GraphDB interface that abstracts SPARQL queries and provides a small set of commonly needed queries.""" + """ + High-level client for GraphDB repositories. + + Provides convenience methods for querying and updating data, manages + authentication, repository selection, a default named graph, and a + Kafka connector manager. + """ def __init__( self, credentials: GraphDBCredentials, timeout: int = 60, use_gdb_token: bool = True, - named_graph: Optional[str] = None, + named_graph: Optional[Union[str, IRI]] = None, logger: Optional[logging.Logger] = None, ): if logger is None: @@ -47,12 +54,6 @@ def __init__( self.repository = credentials.repository - self._prefixes = {} - self.add_prefix("owl", "") - self.add_prefix("rdf", "") - self.add_prefix("rdfs", "") - self.add_prefix("onto", "") - self.named_graph = named_graph self.kafka_manager = KafkaManager(db=self) @@ -61,7 +62,16 @@ def __init__( ) @classmethod - def from_env(cls, logger: Optional[logging.Logger] = None) -> "GraphDB": + def from_env( + cls, + logger: Optional[logging.Logger] = None, + ) -> GraphDB: + """ + Construct a client using environment-based credentials. + + Returns: + GraphDB: A configured `GraphDB` instance using `GraphDBCredentials.from_env()`. + """ return cls(credentials=GraphDBCredentials.from_env(), logger=logger) from graph_db_interface.queries.named_graph import ( @@ -81,6 +91,8 @@ def from_env(cls, logger: Optional[logging.Logger] = None) -> "GraphDB": ) from graph_db_interface.queries.triple_multi import ( triples_get, + any_triple_exists, + all_triple_exists, triples_add, triples_delete, triples_update, @@ -93,37 +105,75 @@ def from_env(cls, logger: Optional[logging.Logger] = None) -> "GraphDB": ) @property - def repository(self): - """The currently selected respository in the Graph DB instance.""" + def repository(self) -> str: + """ + The currently selected repository identifier. + + Returns: + str: The active repository id. + """ return self._repository @repository.setter - def repository(self, value: str): + def repository( + self, + value: str, + ): self._repository = self._validate_repository(value) @property - def named_graph(self): - """The currently selected named graph in the Graph DB instance.""" + def named_graph(self) -> Optional[IRI]: + """ + The currently selected default named graph. + + Returns: + Optional[IRI]: The default named graph as an `IRI`, or `None` if unset. + """ return self._named_graph @named_graph.setter - def named_graph(self, value: Optional[str]): - if value is not None: - if utils.strip_angle_brackets(value) not in self.get_list_of_named_graphs(): - self.logger.warning( - f"Passed named graph {value} does not exist in the repository." - ) - self._named_graph = utils.ensure_absolute(value) - else: + def named_graph( + self, + value: Optional[Union[str, IRI]], + ): + if value is None: self._named_graph = None + return + + value = IRI(value) + + if value not in self.get_list_of_named_graphs(): + self.logger.warning( + f"Passed named graph {value} does not exist in the repository." + ) + self._named_graph = value + + @property + def named_graph_str(self) -> Optional[str]: + """ + The selected default named graph as a string. + + Returns: + Optional[str]: The IRI string of the default named graph, or `None`. + """ + if self._named_graph is None: + return None + return str(self._named_graph) def get_list_of_repositories( - self, only_ids: bool = False + self, + only_ids: Optional[bool] = False, ) -> Union[List[str], List[dict], None]: - """Get a list of all existing repositories on the GraphDB instance. + """ + List repositories available on the GraphDB instance. + + Args: + only_ids (Optional[bool]): When True, return only the repository ids. When False, + return the full repository descriptor objects. Defaults to False. Returns: - Optional[List[str]]: Returns a list of repository ids. + Union[List[str], List[dict], None]: A list of ids if `only_ids=True`, a list of + repository descriptors otherwise, or `None` when the request fails. """ response = self._make_request("get", "rest/repositories") @@ -138,8 +188,22 @@ def get_list_of_repositories( ) return None - def _validate_repository(self, repository: str) -> str: - """Validates if the repository is part of the RepositoryNames enum.""" + def _validate_repository( + self, + repository: str, + ) -> str: + """ + Validate that the repository exists on the server. + + Args: + repository (str): The repository identifier to validate. + + Returns: + str: The validated repository identifier. + + Raises: + InvalidRepositoryError: If the repository is not available. + """ if repository not in self._repositories: raise InvalidRepositoryError( "Invalid repository name. Allowed values are:" @@ -148,8 +212,24 @@ def _validate_repository(self, repository: str) -> str: return repository def _make_request( - self, method: str, endpoint: str, timeout: int = None, **kwargs + self, + method: str, + endpoint: str, + timeout: Optional[int] = None, + **kwargs, ) -> Response: + """ + Perform an authenticated HTTP request to the GraphDB REST API. + + Args: + method (str): The HTTP method (e.g., "get", "post"). + endpoint (str): The REST endpoint path relative to `base_url`. + timeout (Optional[int]): Request timeout in seconds; defaults to the client setting. + **kwargs: Additional arguments forwarded to `requests`. + + Returns: + Response: The `requests.Response` object. + """ timeout = timeout if timeout is not None else self._timeout headers = kwargs.pop("headers", {}) @@ -164,18 +244,23 @@ def _make_request( **kwargs, ) - def _get_authentication_token(self, username: str, password: str) -> str: - """Obtain a GDB authentication token given your username and your password + def _get_authentication_token( + self, + username: str, + password: str, + ) -> str: + """ + Obtain a GraphDB authentication token. Args: - username (str): username of your GraphDB account - password (str): password of your GraphDB account - - Raises: - ValueError: raised when no token could be successfully obtained + username (str): The GraphDB username. + password (str): The GraphDB password. Returns: - str: gdb token + str: The `Authorization` header value (e.g., `Bearer ...`). + + Raises: + AuthenticationError: If a token cannot be obtained with the provided credentials. """ payload = { "username": username, @@ -193,49 +278,26 @@ def _get_authentication_token(self, username: str, password: str) -> str: " Please make sure, that your provided credentials are valid." ) - def _get_prefix_string(self) -> str: - return ( - "\n".join( - f"PREFIX {prefix}: {iri}" for prefix, iri in self._prefixes.items() - ) - + "\n" - ) - - def _named_graph_string(self, named_graph: str = None) -> str: - if named_graph: - return f"GRAPH {named_graph}" - - return "" - - def add_prefix(self, prefix: str, iri: str): - self._prefixes[prefix] = utils.ensure_absolute(iri) - - def remove_prefix(self, prefix: str) -> bool: - if prefix in self._prefixes: - del self._prefixes[prefix] - return True - return False - - def get_prefixes(self) -> Dict[str, str]: - return self._prefixes - def query( self, query: str, update: bool = False, ) -> Optional[Union[Dict, bool]]: """ - Executes a SPARQL query or update operation on the GraphDB repository. + Execute a SPARQL query or update against the repository. + Args: - query (str): The SPARQL query or update string to be executed. - update (bool, optional): Indicates whether the query is an update operation. + query (str): The SPARQL query/update string to execute. + update (bool): If True, perform an update; otherwise perform a read query. Defaults to False. + Returns: - Optional[Union[Dict, bool]]: - - If `update` is False, returns the query result as a dictionary (parsed JSON). - - If `update` is True, returns True if the update was successful. - - Returns None if the query fails and `update` is False. - - Returns False if the update fails and `update` is True. + Optional[Union[Dict, bool]]: If `update` is False, the parsed JSON result dict. + If `update` is True, `True` on success. Returns `None` or `False` only when + failures are handled upstream; otherwise an exception is raised. + + Raises: + GraphDbException: If the HTTP request succeeds but the GraphDB API returns an error status. """ endpoint = f"repositories/{self._repository}" headers = { @@ -257,4 +319,8 @@ def query( f"Error while querying GraphDB ({status_code}) - {response.text}" ) + self.logger.debug( + f'Query\n"""\n{query}\n"""\nReturned\n{"Update successful (200)" if update else response.json()}' + ) + return True if update else response.json() diff --git a/graph_db_interface/kafka/kafka_manager.py b/graph_db_interface/kafka/kafka_manager.py index d1f5be1..8ae1ea6 100644 --- a/graph_db_interface/kafka/kafka_manager.py +++ b/graph_db_interface/kafka/kafka_manager.py @@ -9,35 +9,34 @@ class KafkaManager: """ - A manager for Kafka connectors in GraphDB. + Manage GraphDB Kafka connectors via SPARQL. + + Provides helpers to list, inspect, create, and drop Kafka connectors stored in + GraphDB following Ontotext's connector ontology. Reference: - https://graphdb.ontotext.com/documentation/11.1/kafka-graphdb-connector.html + https://graphdb.ontotext.com/documentation/11.1/kafka-graphdb-connector.html """ - def __init__(self, db: "GraphDB"): + def __init__( + self, + db: "GraphDB", + ): self.db = db - self.db.add_prefix("kafka", "") - self.db.add_prefix( - "kafka-inst", "" - ) self.logger = logging.getLogger(self.__class__.__name__) self.logger.info("KafkaManager initialized") def get_existing_connector_ids(self) -> List[str]: """ - Get the IDs of existing Kafka connectors from the graph database. + Get the IDs of existing Kafka connectors. - This method queries the graph database using SPARQL to retrieve all connector - IDs that are registered in the system. It constructs a SELECT query that looks - for resources with the kafka:listConnectors predicate. - - Args: - None + Queries the graph database for resources with the `kafka:listConnectors` predicate. Returns: - List[str]: A list of connector ID strings. Returns an empty list if no - connectors are found in the database. + List[str]: Connector IDs; empty when none are found. + + Raises: + GraphDbException: If the underlying query execution fails. """ query = SPARQLQuery(prefixes=self.db.get_prefixes()) query.add_select_block( @@ -49,21 +48,23 @@ def get_existing_connector_ids(self) -> List[str]: return [res["cntStr"]["value"] for res in results["results"]["bindings"]] def get_status_of_connectors( - self, id: Optional[str] = None + self, + id: Optional[str] = None, ) -> Optional[Dict[str, Dict]]: """ - Get the status of Kafka connectors from the graph database. + Get the status of Kafka connectors. - This method queries the graph database for connector status information. It can retrieve - the status of all connectors or a specific connector if an ID is provided. + When `id` is provided, returns the status of that connector; otherwise returns + statuses for all connectors. Args: - id (Optional[str], optional): The ID of a specific connector to query. If None, - retrieves the status of all connectors. Defaults to None. + id (Optional[str]): Connector id to filter by. Defaults to None. Returns: - Optional[Dict[str, Dict]]: A dictionary mapping connector names to their status - information. Returns None if no connectors are found or the query returns no results. + Optional[Dict[str, Dict]]: Mapping of connector name to status; `None` when no results. + + Raises: + GraphDbException: If the underlying query execution fails. """ query = SPARQLQuery(prefixes=self.db.get_prefixes()) query.add_select_block( @@ -85,20 +86,24 @@ def get_status_of_connectors( else: return None - def get_connector_create_options(self, id: str) -> Optional[Dict]: + def get_connector_create_options( + self, + id: str, + ) -> Optional[str]: """ - Retrieve the create options for a Kafka connector from the graph database. + Retrieve the creation options for a Kafka connector. - This method queries the graph database to fetch the creation configuration string - associated with a specific Kafka connector instance. + Queries the graph database for the stored creation configuration string for the + given connector instance. Args: - id (str): The identifier of the Kafka connector instance. + id (str): The connector identifier. Returns: - Optional[Dict]: The create options string if found, None otherwise. - Note: Despite the return type hint suggesting Dict, this method - returns a string value from the query results or None. + Optional[str]: The creation options string when available, otherwise `None`. + + Raises: + GraphDbException: If the underlying query execution fails. """ query = SPARQLQuery(prefixes=self.db.get_prefixes()) query.add_select_block( @@ -111,12 +116,18 @@ def get_connector_create_options(self, id: str) -> Optional[Dict]: return results["results"]["bindings"][0]["createString"]["value"] return None - def drop_connector(self, id: str) -> bool: + def drop_connector( + self, + id: str, + ) -> bool: """ - Drops the specified Kafka connector. + Drop the specified Kafka connector. + + Args: + id (str): Connector identifier to drop. Returns: - bool: True if the connector was dropped successfully, False otherwise. + bool: True on success, False otherwise. """ query = SPARQLQuery(prefixes=self.db.get_prefixes()) query.add_insert_data_block( @@ -136,26 +147,21 @@ def drop_connector(self, id: str) -> bool: return False def create_connector( - self, id: str, connector_config: dict, overwrite: bool = False - ): + self, + id: str, + connector_config: dict, + overwrite: Optional[bool] = False, + ) -> None: """ - Create a Kafka connector with the specified ID and configuration. + Create a Kafka connector with the specified configuration. - This method creates a new Kafka connector by inserting the connector configuration - into the graph database using a SPARQL INSERT DATA query. If a connector with the - same ID already exists and overwrite is True, the existing connector will be dropped - before creating the new one. + Inserts the connector configuration into GraphDB via an INSERT DATA query. If a + connector with the same id exists and `overwrite=True`, it will be dropped first. Args: - id (str): The unique identifier for the Kafka connector. - connector_config (dict): A dictionary containing the configuration parameters - for the Kafka connector. This will be serialized to JSON and stored in - the database. - overwrite (bool, optional): If True, drops any existing connector with the - same ID before creating the new one. Defaults to False. - - Returns: - None + id (str): Unique connector identifier. + connector_config (dict): Kafka connector configuration to serialize and store. + overwrite (Optional[bool]): Drop any existing connector with the same id first. Defaults to False. """ if overwrite and id in self.get_existing_connector_ids(): self.drop_connector(id) diff --git a/graph_db_interface/queries/named_graph.py b/graph_db_interface/queries/named_graph.py index b96c45e..3211f26 100644 --- a/graph_db_interface/queries/named_graph.py +++ b/graph_db_interface/queries/named_graph.py @@ -1,28 +1,35 @@ # To be imported into ..graph_db.py GraphDB class -from typing import List, Optional, TYPE_CHECKING +from typing import List, TYPE_CHECKING +from graph_db_interface.utils.iri import IRI if TYPE_CHECKING: from graph_db_interface import GraphDB -def get_list_of_named_graphs(self: "GraphDB") -> Optional[List]: - """Get a list of named graphs in the currently set repository. +def get_list_of_named_graphs( + self: "GraphDB", +) -> List[IRI]: + """ + Get the list of named graphs in the current repository. Returns: - Optional[List]: List of named graph IRIs. Can be an empty list. + List[IRI]: List of named graph IRIs. Can be an empty list. + + Raises: + GraphDbException: If the underlying request to GraphDB fails. """ # TODO: This query is quite slow and should be optimized # SPARQL query to retrieve all named graphs query = """ - SELECT DISTINCT ?graph WHERE { +SELECT DISTINCT ?graph WHERE { GRAPH ?graph { ?s ?p ?o } - } +} """ results = self.query(query) if results is None: return [] - return [result["graph"]["value"] for result in results["results"]["bindings"]] + return [IRI(result["graph"]["value"]) for result in results["results"]["bindings"]] diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index 5ed992a..5e5969e 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -1,8 +1,10 @@ # To be imported into ..graph_db.py GraphDB class -from typing import List, Optional, TYPE_CHECKING +from typing import List, Optional, Union, TYPE_CHECKING from graph_db_interface.utils import utils +from graph_db_interface.utils.iri import IRI from graph_db_interface.exceptions import InvalidInputError +from rdflib import Namespace from graph_db_interface.sparql_query import SPARQLQuery @@ -12,29 +14,29 @@ def iri_exists( self: "GraphDB", - iri: str, - as_sub: bool = False, - as_pred: bool = False, - as_obj: bool = False, - include_explicit: bool = True, - include_implicit: bool = True, + iri: Union[str, IRI], + as_sub: Optional[bool] = False, + as_pred: Optional[bool] = False, + as_obj: Optional[bool] = False, + include_explicit: Optional[bool] = True, + include_implicit: Optional[bool] = True, ) -> bool: """ - Checks if a given IRI exists in the graph database as a subject, predicate, or object. + Check if an IRI exists as subject, predicate, or object. Args: - iri (str): The IRI to check for existence. - as_sub (bool, optional): If True, checks if the IRI exists as a subject. Defaults to False. - as_pred (bool, optional): If True, checks if the IRI exists as a predicate. Defaults to False. - as_obj (bool, optional): If True, checks if the IRI exists as an object. Defaults to False. - include_explicit (bool, optional): If True, includes explicitly defined triples in the query. Defaults to True. - include_implicit (bool, optional): If True, includes implicitly inferred triples in the query. Defaults to True. + iri (Union[str, IRI]): The IRI to check for existence. + as_sub (Optional[bool]): If True, check existence as subject. Defaults to False. + as_pred (Optional[bool]): If True, check existence as predicate. Defaults to False. + as_obj (Optional[bool]): If True, check existence as object. Defaults to False. + include_explicit (Optional[bool]): Include explicit triples (`FROM onto:explicit`). Defaults to True. + include_implicit (Optional[bool]): Include inferred triples (`FROM onto:implicit`). Defaults to True. Returns: - bool: True if the IRI exists in the graph database based on the specified criteria, False otherwise. + bool: True if the IRI exists based on the specified criteria, False otherwise. Raises: - InvalidInputError: If none of `as_sub`, `as_pred`, or `as_obj` is set to True. + InvalidInputError: If none of `as_sub`, `as_pred`, or `as_obj` is True. """ # Check if either as_subject, as_predicate, or as_object is True @@ -43,21 +45,18 @@ def iri_exists( "At least one of as_sub, as_pred, or as_obj must be True" ) + iri = IRI(iri) + # Define potential query parts where_clauses = [] if as_sub: - sub = utils.prepare_subject(iri, ensure_iri=True) - where_clauses.append(f"{{{sub} ?p ?o . }}") + where_clauses.append(f"{{{iri.n3()} ?p ?o . }}") if as_pred: - pred = utils.prepare_predicate(iri, ensure_iri=True) - where_clauses.append(f"{{?s {pred} ?o . }}") + where_clauses.append(f"{{?s {iri.n3()} ?o . }}") if as_obj: - obj = utils.prepare_object(iri, as_string=True) - where_clauses.append(f"{{?s ?p {obj} . }}") - + where_clauses.append(f"{{?s ?p {iri.n3()} . }}") query = SPARQLQuery( - named_graph=self._named_graph, - prefixes=self._prefixes, + named_graph=self.named_graph, include_explicit=include_explicit, include_implicit=include_implicit, ) @@ -80,77 +79,67 @@ def iri_exists( return False -def is_subclass(self: "GraphDB", subclass_iri: str, class_iri: str) -> bool: +def is_subclass( + self: "GraphDB", + subclass_iri: Union[str, IRI], + class_iri: Union[str, IRI], +) -> bool: """ - Determines whether a given class (subclass_iri) is a subclass of another class (class_iri) - based on the "rdfs:subClassOf" relationship. + Check whether one class is a subclass of another (`rdfs:subClassOf`). + + Asks for `subclass_iri rdfs:subClassOf class_iri` Args: - subclass_iri (str): The IRI of the potential subclass. - class_iri (str): The IRI of the potential superclass. + subclass_iri (Union[str, IRI]): The IRI of the potential subclass. + class_iri (Union[str, IRI]): The IRI of the potential superclass. Returns: - bool: True if subclass_iri is a subclass of class_iri, False otherwise. + bool: True if `subclass_iri` is a subclass of `class_iri`, False otherwise. """ - return self.triple_exists(subclass_iri, "rdfs:subClassOf", class_iri) + return self.triple_exists((subclass_iri, "rdfs:subClassOf", class_iri)) -def owl_is_named_individual(self: "GraphDB", iri: str) -> bool: +def owl_is_named_individual(self: "GraphDB", iri: Union[str, IRI]) -> bool: """ - Checks if the given IRI corresponds to an OWL named individual. + Check if the given IRI corresponds to an OWL named individual. - This method verifies whether the provided IRI is explicitly defined as - an `owl:NamedIndividual` in the RDF graph by checking for the existence - of the triple (IRI, rdf:type, owl:NamedIndividual). If the triple does - not exist, a warning is logged. + Asks for `iri rdf:type owl:NamedIndividual`. Args: - iri (str): The IRI to be checked. + iri (Union[str, IRI]): The IRI to check. Returns: bool: True if the IRI is a named individual, False otherwise. """ - if not self.triple_exists(iri, "rdf:type", "owl:NamedIndividual"): - self.logger.debug(f"IRI {iri} is not a named individual!") - return False - return True + return self.triple_exists((iri, "rdf:type", "owl:NamedIndividual")) def owl_get_classes_of_individual( self: "GraphDB", - instance_iri: str, - ignored_prefixes: Optional[List[str]] = None, - local_name: bool = False, - include_explicit=True, - include_implicit=False, -) -> List[str]: + instance_iri: Union[str, IRI], + ignored_prefixes: Optional[List[Namespace]] = None, + local_name: Optional[bool] = False, + include_explicit: Optional[bool] = True, + include_implicit: Optional[bool] = False, +) -> List[Union[IRI, str]]: """ - Retrieves the OWL classes associated with a given individual (instance IRI) - from a graph database. + Get the OWL classes associated with a given individual. + + Builds a SPARQL query that returns the classes for an instance IRI and + optionally filters out results by prefix or returns local names only. Args: - instance_iri (str): The IRI of the individual whose classes are to be retrieved. - ignored_prefixes (Optional[List[str]]): A list of prefixes to ignore when - filtering classes. Defaults to ["owl", "rdfs"] if not provided. - local_name (bool): If True, returns the local names of the classes - (i.e., the part of the IRI after the last '#', '/', or ':'). - Defaults to False. - include_explicit (bool): If True, includes explicitly defined triples in the query. - Defaults to True. - include_implicit (bool): If True, includes implicitly inferred triples in the query. - Defaults to False. + instance_iri (Union[str, IRI]): IRI of the individual to inspect. + ignored_prefixes (Optional[List[Namespace]]): Prefixes/namespaces to ignore + when collecting classes. Defaults to ["owl", "rdfs"]. + local_name (Optional[bool]): If True, return only the local names of the classes. Defaults to False. + include_explicit (Optional[bool]): Include explicit triples. Defaults to True. + include_implicit (Optional[bool]): Include inferred triples. Defaults to False. Returns: - List[str]: A list of class IRIs or local names (depending on the value - of `local_name`) associated with the given individual. - - Notes: - - The method constructs a SPARQL query to retrieve the classes of the - individual and applies optional filtering based on ignored prefixes. - - If no results are found, an empty list is returned. - - The `utils.get_local_name` function is used to extract the local name - from the IRI if `local_name` is set to True. + List[Union[IRI, str]]: Class IRIs, or local names if `local_name=True`. """ + instance_iri = IRI(instance_iri) ignored_prefixes = ( ignored_prefixes if ignored_prefixes is not None else ["owl", "rdfs"] ) @@ -170,8 +159,7 @@ def owl_get_classes_of_individual( filter_conditions = "" query = SPARQLQuery( - named_graph=self._named_graph, - prefixes=self._prefixes, + named_graph=self.named_graph, include_explicit=include_explicit, include_implicit=include_implicit, ) @@ -180,7 +168,7 @@ def owl_get_classes_of_individual( variables=["?class"], where_clauses=[ f"?class rdf:type owl:Class .", - f"{utils.prepare_subject(instance_iri)} rdf:type ?class .", + f"{instance_iri.n3()} rdf:type ?class .", filter_conditions, ], ) @@ -192,7 +180,14 @@ def owl_get_classes_of_individual( if results is None: return [] - classes = [result["class"]["value"] for result in results["results"]["bindings"]] - if local_name is True: - classes = [utils.get_local_name(iri) for iri in classes] + if local_name: + classes = [ + utils.get_local_name(result["class"]["value"]) + for result in results["results"]["bindings"] + ] + return classes + + classes = [ + IRI(result["class"]["value"]) for result in results["results"]["bindings"] + ] return classes diff --git a/graph_db_interface/queries/rdf4j/graph_store.py b/graph_db_interface/queries/rdf4j/graph_store.py index 3ff93fe..78462ec 100644 --- a/graph_db_interface/queries/rdf4j/graph_store.py +++ b/graph_db_interface/queries/rdf4j/graph_store.py @@ -1,6 +1,7 @@ from requests import Response -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING, Optional, Union from rdflib import Graph +from graph_db_interface.utils.iri import IRI if TYPE_CHECKING: from graph_db_interface import GraphDB @@ -8,115 +9,155 @@ def fetch_statements( self: "GraphDB", - graph_uri: Optional[str] = None, -) -> Tuple[Response, Graph]: + graph_iri: Optional[Union[str, IRI]] = None, +) -> Optional[Graph]: """ - Fetch the contents of either a explicit named or the default graph. - If graph_uri is None, the default graph is fetched. + Fetch statements from a named or the default graph. + + Queries the RDF4J Graph Store endpoint and parses the response into an + `rdflib.Graph`. When `graph_iri` is None, the default graph is fetched. + + Args: + graph_iri (Optional[Union[str, IRI]]): The named graph IRI to fetch; when + None, the default graph is fetched. + + Returns: + Optional[Graph]: The parsed graph on success; `None` when the request fails. + + Raises: + requests.exceptions.RequestException: If the underlying HTTP request fails. """ - default_graph = True if graph_uri is None else False + graph_iri_str = ( + str(IRI(graph_iri)) if graph_iri else "http://www.openrdf.org/schema/sesame#nil" + ) g = Graph() endpoint = f"repositories/{self._repository}/rdf-graphs/service" - if graph_uri is None: - graph_uri = "http://www.openrdf.org/schema/sesame#nil" response: Response = self._make_request( "get", endpoint, - params={"graph": graph_uri} if graph_uri else None, + params={"graph": graph_iri_str}, headers={"Content-Type": "application/x-turtle"}, ) - if response.status_code == 200: - if not default_graph: - self.logger.debug(f"Named graph {graph_uri} fetched successfully!") - else: - self.logger.debug("Default graph fetched successfully!") - g.parse(data=response.text, format="nt") - else: - if not default_graph: - self.logger.warning( - f"Failed to fetch named graph: {response.status_code} -" - f" {response.text}" - ) - else: - self.logger.warning( - f"Failed to fetch default graph: {response.status_code} -" - f" {response.text}" - ) - return response, g + + if response.status_code != 200: + self.logger.warning( + f"Failed to fetch named graph: {response.status_code} - {response.text}" + if graph_iri_str + else f"Failed to fetch default graph: {response.status_code} - {response.text}" + ) + return None + + self.logger.debug( + f"Named graph '{graph_iri_str}' fetched successfully!" + if graph_iri_str + else "Default graph fetched successfully!" + ) + g.parse(data=response.text, format="nt") + return g def import_statements( self: "GraphDB", content: str, - overwrite: bool = False, - graph_uri: Optional[str] = None, - content_type: str = "application/x-turtle", -): - default_graph = True if graph_uri is None else False - if default_graph: - endpoint = f"repositories/{self._repository}/rdf-graphs/service?default" - else: + overwrite: Optional[bool] = False, + graph_iri: Optional[Union[str, IRI]] = None, + content_type: Optional[str] = "application/x-turtle", +) -> bool: + """ + Import RDF statements into a named or the default graph. + + Sends content to the RDF4J Graph Store endpoint using POST (append) or PUT + (overwrite). When `graph_iri` is None, the default graph is targeted. + + Args: + content (str): RDF content to import. + overwrite (Optional[bool]): Use PUT to overwrite existing content. Defaults to False. + graph_iri (Optional[Union[str, IRI]]): Target named graph IRI; default graph when None. + content_type (Optional[str]): MIME type of `content` (e.g., 'application/x-turtle'). + Defaults to 'application/x-turtle'. + + Returns: + bool: True on success (HTTP 204), False otherwise. + + Raises: + requests.exceptions.RequestException: If the underlying HTTP request fails. + """ + graph_iri_str = str(IRI(graph_iri)) if graph_iri else None + if graph_iri_str: endpoint = f"repositories/{self._repository}/rdf-graphs/service" + else: + endpoint = f"repositories/{self._repository}/rdf-graphs/service?default" method = "put" if overwrite else "post" response: Response = self._make_request( method, endpoint, - params={"graph": graph_uri} if graph_uri else None, + params={"graph": graph_iri_str} if graph_iri_str else None, headers={"Content-Type": content_type}, data=content, ) - if response.status_code == 204: - if not default_graph: - self.logger.debug( - f"Statements imported to named graph {graph_uri} successfully!" - ) - else: - self.logger.debug("Statements imported to default graph successfully!") - else: - if not default_graph: - self.logger.warning( - f"Failed to import statements to named graph: {response.status_code} -" - f" {response.text}" - ) - else: - self.logger.warning( - f"Failed to import statements to default graph: {response.status_code} -" - f" {response.text}" - ) - return response - - -def clear_graph(self: "GraphDB", graph_uri: Optional[str] = None): + + if response.status_code != 204: + self.logger.warning( + f"Failed to import statements to named graph: {response.status_code} - {response.text}" + if graph_iri_str + else f"Failed to import statements to default graph: {response.status_code} - {response.text}" + ) + return False + + self.logger.debug( + f"Named graph {graph_iri_str} imported successfully!" + if graph_iri_str + else "Default graph imported successfully!" + ) + return True + + +def clear_graph( + self: "GraphDB", + graph_iri: Optional[Union[str, IRI]] = None, +) -> bool: """ - Deletes the specified named graph from the triplestore. + Clear a named graph or the default graph. + + Deletes the specified named graph from the triplestore; when `graph_iri` is + None, clears the default graph. + + Args: + graph_iri (Optional[Union[str, IRI]]): IRI of the named graph to clear; + default graph when None. + + Returns: + bool: True on success (HTTP 204), False otherwise. + + Raises: + requests.exceptions.RequestException: If the underlying HTTP request fails. """ - default_graph = True if graph_uri is None else False - if default_graph: - endpoint = f"repositories/{self._repository}/rdf-graphs/service?default" - else: + graph_iri_str = str(IRI(graph_iri)) if graph_iri else None + if graph_iri_str: endpoint = f"repositories/{self._repository}/rdf-graphs/service" + else: + endpoint = f"repositories/{self._repository}/rdf-graphs/service?default" response: Response = self._make_request( "delete", endpoint, - params={"graph": graph_uri} if graph_uri else None, + params={"graph": graph_iri_str} if graph_iri_str else None, ) - if response.status_code == 204: - if not default_graph: - self.logger.debug(f"Named graph {graph_uri} cleared successfully!") - else: - self.logger.debug(f"Default graph cleared successfully!") - else: - if not default_graph: - self.logger.warning( - f"Failed to clear named graph: {response.status_code} - {response.text}" - ) - else: - self.logger.warning( - f"Failed to clear default graph: {response.status_code} - {response.text}" - ) - return response + if response.status_code != 204: + self.logger.warning( + f"Failed to clear named graph: {response.status_code} - {response.text}" + if graph_iri_str + else f"Failed to clear default graph: {response.status_code} - {response.text}" + ) + return False + + self.logger.debug( + f"Named graph {graph_iri_str} cleared successfully!" + if graph_iri_str + else "Default graph cleared successfully!" + ) + return True diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index 121d052..9aadc4e 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -3,6 +3,8 @@ from typing import List, Union, Any, Optional, Tuple, TYPE_CHECKING from rdflib import Literal from graph_db_interface.utils import utils +from graph_db_interface.utils.iri import IRI +from graph_db_interface.utils.utils import Triple, PartialTriple from graph_db_interface.exceptions import InvalidInputError from graph_db_interface.sparql_query import SPARQLQuery @@ -13,63 +15,70 @@ def triples_get( self: "GraphDB", - sub: Optional[str] = None, - pred: Optional[str] = None, - obj: Optional[Any] = None, - include_explicit: bool = True, - include_implicit: bool = True, -) -> Union[List[Tuple], List[str]]: + triple: Optional[PartialTriple] = None, + sub: Optional[Union[str, IRI]] = None, + pred: Optional[Union[str, IRI]] = None, + obj: Optional[Union[str, IRI, Literal]] = None, + include_explicit: Optional[bool] = True, + include_implicit: Optional[bool] = True, +) -> List[Tuple[IRI, IRI, Any]]: """ - Retrieve triples based on the specified subject, predicate, and/or object. + Retrieve triples matching any combination of subject, predicate, or object. Args: - sub (Optional[str]): The subject of the triple. Can be an IRI, shorthand IRI, or a string. - pred (Optional[str]): The predicate of the triple. Can be an IRI, shorthand IRI, or a string. - obj (Optional[Any]): The object of the triple. Can be an IRI, shorthand IRI, Literal, or a string. - include_explicit (bool): Whether to include explicitly defined triples. Defaults to True. - include_implicit (bool): Whether to include implicitly inferred triples. Defaults to True. + triple (Optional[PartialTriple]): Combined (subject, predicate, object) filter tuple. Use this + or individual `sub`/`pred`/`obj`. + sub (Optional[Union[str, IRI]]): Subject filter (IRI/shorthand/string). + pred (Optional[Union[str, IRI]]): Predicate filter (IRI/shorthand/string). + obj (Optional[Union[str, IRI, Literal]]): Object filter (IRI/shorthand/Literal/string). + include_explicit (Optional[bool]): Include explicit triples. Defaults to True. + include_implicit (Optional[bool]): Include inferred triples. Defaults to True. Returns: - Union[List[Tuple], List[str]]: A list of triples matching the query. Each triple is represented as a tuple - (subject, predicate, object), where the object is converted to its Python type if applicable. + List[Tuple[IRI, IRI, Any]]: Matching triples as `(subject, predicate, object)`, where the + object is converted to an appropriate Python type when applicable. Raises: - InvalidInputError: If none of the subject, predicate, or object is provided. + InvalidInputError: If neither or both of `triple` and any of `sub`/`pred`/`obj` are provided. """ - if sub is None and pred is None and obj is None: + elems_given = sub is not None or pred is not None or obj is not None + if (triple and elems_given) or (not triple and not elems_given): raise InvalidInputError( - "At least one of subject, predicate, or object must be provided" + "Either 'triple' or 'sub/pred/obj' must be provided, not both." ) + sub, pred, obj = utils.sanitize_triple( + triple or (sub, pred, obj), allow_partial=True + ) + binds = [] filter = [] - def append_bind_and_filter(var: str, value: str): - if utils.is_iri(value): - binds.append(f"BIND({utils.ensure_absolute(value)} AS {var})") - elif utils.is_shorthand_iri(value): - binds.append(f"BIND({value} AS {var})") + def _append_bind_and_filter( + var: str, + value: Union[IRI, Literal], + ) -> None: + if isinstance(value, IRI): + binds.append(f"BIND({value.n3()} AS {var})") elif isinstance(value, Literal): filter.append(f"FILTER(?o={value.n3()})") else: - filter.append(f"FILTER(CONTAINS(STR({var}), '{value}'))") + raise Exception( + f"Value must be either IRI or Literal, is type {type(value)}" + ) if sub is not None: - sub = utils.prepare_subject(sub, ensure_iri=False) - append_bind_and_filter("?s", sub) + _append_bind_and_filter("?s", sub) if pred is not None: - pred = utils.prepare_predicate(pred, ensure_iri=False) - append_bind_and_filter("?p", pred) + _append_bind_and_filter("?p", pred) if obj is not None: - obj = utils.prepare_object(obj, ensure_iri=False) - append_bind_and_filter("?o", obj) + _append_bind_and_filter("?o", obj) query = SPARQLQuery( - named_graph=self._named_graph, # type: ignore - prefixes=self._prefixes, + named_graph=self.named_graph, include_explicit=include_explicit, include_implicit=include_implicit, ) @@ -87,8 +96,8 @@ def append_bind_and_filter(var: str, value: str): results = self.query(query=query_string) converted_results = [ ( - result["s"]["value"], - result["p"]["value"], + IRI(result["s"]["value"]), + IRI(result["p"]["value"]), utils.convert_query_result_to_python_type(result["o"]), ) for result in results["results"]["bindings"] @@ -96,228 +105,268 @@ def append_bind_and_filter(var: str, value: str): return converted_results -def triples_add( - self, - triples_to_add: List[Tuple[str, str, Any]], - check_exist: bool = True, - named_graph: Optional[str] = None, +def any_triple_exists( + self: "GraphDB", + triples: List[Triple], + named_graph: Optional[IRI] = None, ) -> bool: """ - Adds multiple triples to the graph database. + Check if any of the given triples exist. Args: - triples_to_add (List[Tuple[str, str, Any]]): A list of triples to add, where each triple is represented as a tuple (subject, predicate, object). - check_exist (bool, optional): Flag to check if any of the triples already exists. Then, no triple will be added. In Defaults to True. + triples (List[Triple]): Triples to check. + named_graph (Optional[IRI]): Override the client's default named graph. Returns: - bool: True if all triples were successfully added, False otherwise. + bool: True if at least one exists, False otherwise. """ - if not triples_to_add: - raise InvalidInputError("The list of triples to add must not be empty.") - prepared_triples = [] + if not triples: + raise InvalidInputError(f"Cannot check existence of empty triple list.") + + for triple in triples: + triple = utils.sanitize_triple(triple) + + if self.triple_exists(triple): + self.logger.debug( + f"At least this triple exists: ({utils.triple_to_string(triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + return True + + self.logger.debug( + f"None of the triples exists: ({triples}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + return False + + +def all_triple_exists( + self: "GraphDB", + triples: List[Triple], + named_graph: Optional[IRI] = None, +) -> bool: + """ + Check if all of the given triples exist. - for sub, pred, obj in triples_to_add: - sub = utils.prepare_subject(sub, ensure_iri=True) - pred = utils.prepare_predicate(pred, ensure_iri=True) - obj = utils.prepare_object(obj, as_string=True) + Args: + triples (List[Triple]): Triples to check. + named_graph (Optional[IRI]): Override the client's default named graph. + + Returns: + bool: True if all exist, False otherwise. + """ - if not sub or not pred or not obj: - raise InvalidInputError(f"Invalid triple: ({sub}, {pred}, {obj})") - return False - prepared_triples.append((sub, pred, obj)) + if not triples: + raise InvalidInputError(f"Cannot check existence of empty triple list.") - if check_exist: - ask_query = SPARQLQuery( - named_graph=self._named_graph, - prefixes=self._prefixes, + triple_strings = [] + for triple in triples: + triple = utils.sanitize_triple(triple) + triple_strings.append(utils.triple_to_string(triple, ".")) + + query = SPARQLQuery(named_graph=named_graph or self.named_graph) + query.add_ask_block(where_clauses=triple_strings) + query_string = query.to_string() + if not query_string: + raise InvalidInputError( + f"Could not generate 'all_triple_exists' query for triples ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" ) - ask_query.add_ask_block( - where_clauses=[ - f"{sub} {pred} {obj} ." for sub, pred, obj in prepared_triples - ], + + ask_result = self.query(query=query_string, update=False) + if ask_result is None: + raise InvalidInputError( + f"Could not query 'all_triple_exists' for triples ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" ) - ask_query_string = ask_query.to_string() - if ask_query_string is None: - return False - ask_result = self.query(query=ask_query_string, update=False) - if ask_result is not None and ask_result["boolean"]: - self.logger.warning( - "One of the triples to add already exists in the graph." - ) - return False - query = SPARQLQuery( - named_graph=self._named_graph, - prefixes=self._prefixes, - ) - query.add_insert_data_block( - triples=prepared_triples, + if ask_result["boolean"] is False: + self.logger.debug( + f"Not all of the triples exist: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + return False + + self.logger.debug( + f"All of the triples exist: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" ) + return True - # if check_exist: - # query.add_insert_exists_block( - # triples=prepared_triples, - # ) - # else: - # query.add_insert_data_block( - # triples=prepared_triples, - # ) - query_string = query.to_string() - if query_string is None: +def triples_add( + self: "GraphDB", + triples_to_add: List[Triple], + check_exist: Optional[bool] = True, + named_graph: Optional[IRI] = None, +) -> bool: + """ + Add multiple triples to the graph database. + + Args: + triples_to_add (List[Triple]): Triples to add. + check_exist (Optional[bool]): If True, abort when any triple already exists. Defaults to True. + named_graph (Optional[IRI]): Override the client's default named graph. + + Returns: + bool: True if all triples were added, False otherwise. + """ + if not triples_to_add: + return True + + validated_triples_to_add = [ + utils.sanitize_triple(triple) for triple in triples_to_add + ] + + if check_exist and self.any_triple_exists( + triples=validated_triples_to_add, named_graph=named_graph + ): + self.logger.warning( + "At least one of the triples to add already exists in the graph." + ) return False - if named_graph: - old_named_graph = self._named_graph - self.named_graph = named_graph + triple_strings = [ + utils.triple_to_string(triple, ".") for triple in validated_triples_to_add + ] + + query = SPARQLQuery(named_graph=named_graph or self.named_graph) + query.add_insert_data_block( + triples=validated_triples_to_add, + ) + query_string = query.to_string() + if not query_string: + raise InvalidInputError( + f"Could not generate 'triples_add' query for triples ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) result = self.query(query=query_string, update=True) if not result: - self.logger.warning(f"Failed to add triples: {prepared_triples}") + self.logger.warning( + f"Failed to add triples: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) return False - if named_graph: - self.named_graph = named_graph - + self.logger.debug( + f"Successfully added triples: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) return result def triples_delete( - self, - triples_to_delete: List[Tuple[str, str, Union[str, Literal]]], - check_exist: bool = True, + self: "GraphDB", + triples_to_delete: List[Triple], + check_exist: Optional[bool] = True, + named_graph: Optional[IRI] = None, ) -> bool: """ Delete multiple triples from the graph database. Args: - triples_to_delete (List[Tuple[str, str, Union[str, Literal]]]): A list of triples to delete, where each triple is represented as a tuple (subject, predicate, object). - check_exist (bool, optional): Flag to check if each triple exists before attempting to delete it. Defaults to True. + triples_to_delete (List[Triple]): Triples to delete. + check_exist (Optional[bool]): If True, abort when any triple does not exist. Defaults to True. + named_graph (Optional[IRI]): Override the client's default named graph. Returns: - bool: Returns True if all triples were successfully deleted, False otherwise. + bool: True if all triples were deleted, False otherwise. """ if not triples_to_delete: - raise InvalidInputError("The list of triples to delete must not be empty.") - - prepared_triples = [] - where_clauses = [] - for sub, pred, obj in triples_to_delete: - sub = utils.prepare_subject(sub, ensure_iri=True) - pred = utils.prepare_predicate(pred, ensure_iri=True) - obj = utils.prepare_object(obj, as_string=True) + return True - if check_exist: + validated_triples_to_delete = [ + utils.sanitize_triple(triple) for triple in triples_to_delete + ] - if not self.triple_exists(sub, pred, obj): - self.logger.warning( - f"Triple does not exist and cannot be deleted: {sub} {pred} {obj}" - ) - return False - where_clauses.append(f"{sub} {pred} {obj} .") - prepared_triples.append((sub, pred, obj)) + if check_exist and not self.all_triple_exists( + triples=validated_triples_to_delete, named_graph=named_graph + ): + self.logger.warning( + "At least one of the triples to delete does not exist in the graph." + ) + return False - query = SPARQLQuery( - named_graph=self._named_graph, - prefixes=self._prefixes, - ) + triple_strings = [ + utils.triple_to_string(triple, ".") for triple in validated_triples_to_delete + ] + query = SPARQLQuery(named_graph=named_graph or self.named_graph) query.add_delete_data_block( - triples=prepared_triples, + triples=validated_triples_to_delete, ) + query_string = query.to_string() + if not query_string: + raise InvalidInputError( + f"Could not generate 'triples_delete' query for triples ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) - result = self.query(query=query.to_string(), update=True) - if result: - self.logger.debug(f"Successfully deleted triples: {prepared_triples}") - else: - self.logger.warning(f"Failed to delete triples: {prepared_triples}") + result = self.query(query=query_string, update=True) + if not result: + self.logger.warning( + f"Failed to delete triples: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + return False - return result + self.logger.debug( + f"Successfully deleted triples: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + return True def triples_update( - self, - old_triples: List[Tuple[str, str, Union[str, Literal]]], - new_triples: List[Tuple[str, str, Union[str, Literal]]], - check_exist: bool = True, + self: "GraphDB", + old_triples: List[Triple], + new_triples: List[Triple], + check_exist: Optional[bool] = True, + named_graph: Optional[IRI] = None, ) -> bool: """ Update multiple RDF triples in the triplestore. Args: - old_triples (List[Tuple[str, str, Union[str, Literal]]]): A list of triples to update, where each triple is represented as a tuple (subject, predicate, object). - new_triples (List[Tuple[Optional[str], Optional[str], Optional[Union[str, Literal]]]]): A list of new triples to replace the old triples. - check_exist (bool): Whether to check for the existence of old triples before updating. + old_triples (List[Triple]): Triples to be replaced. + new_triples (List[Triple]): Replacement triples (same length as `old_triples`). + check_exist (Optional[bool]): If True, abort when any old triple does not exist. Defaults to True. + named_graph (Optional[IRI]): Override the client's default named graph. Returns: bool: True if the update was successful, False otherwise. """ - if not old_triples or not new_triples: - raise InvalidInputError("Old and new triples lists must not be empty.") + if not old_triples and not new_triples: + return True if len(old_triples) != len(new_triples): raise InvalidInputError("Old and new triples lists must have the same length.") - delete_triples = [] - insert_triples = [] - where_clauses = [] + validated_old_triples = [utils.sanitize_triple(triple) for triple in old_triples] + validated_new_triples = [utils.sanitize_triple(triple) for triple in new_triples] - for triple in old_triples: - if len(triple) != 3: - raise InvalidInputError( - "Each old triple must have exactly three elements (subject, predicate, object)." - ) - sub_old, pred_old, obj_old = triple - if check_exist: - if not self.triple_exists(sub_old, pred_old, obj_old): - self.logger.warning( - f"Triple does not exist: {sub_old} {pred_old} {obj_old}" - ) - return False - - sub_old = utils.prepare_subject(sub_old, ensure_iri=True) - pred_old = utils.prepare_predicate(pred_old, ensure_iri=True) - obj_old = utils.prepare_object(obj_old, as_string=True) - delete_triples.append((sub_old, pred_old, obj_old)) - where_clauses.append(f"{sub_old} {pred_old} {obj_old} .") - - for triple in new_triples: - if len(triple) != 3: - raise InvalidInputError( - "Each new triple must have exactly three elements (subject, predicate, object)." - ) - sub_new, pred_new, obj_new = triple + if check_exist and not self.all_triple_exists( + triples=validated_old_triples, named_graph=named_graph + ): + self.logger.warning( + "At least one of the triples to update does not exist in the graph." + ) + return False - if sub_new is not None: - sub_new = utils.prepare_subject(sub_new, ensure_iri=True) - if pred_new is not None: - pred_new = utils.prepare_predicate(pred_new, ensure_iri=True) - if obj_new is not None: - obj_new = utils.prepare_object(obj_new, as_string=True) - insert_triples.append((sub_new, pred_new, obj_new)) + old_triple_strings = [ + utils.triple_to_string(triple, ".") for triple in validated_old_triples + ] - query = SPARQLQuery( - named_graph=self._named_graph, - prefixes=self._prefixes, - ) + query = SPARQLQuery(named_graph=named_graph or self.named_graph) query.add_delete_insert_data_block( - delete_triples=delete_triples, - insert_triples=insert_triples, - where_clauses=where_clauses, + delete_triples=validated_old_triples, + insert_triples=validated_new_triples, + where_clauses=old_triple_strings, ) query_string = query.to_string(validate=True) - if query_string is None: - return False - result = self.query(query=query_string, update=True) - if result: - self.logger.debug( - f"Successfully updated triples {old_triples} -> {new_triples}, named_graph: {self._named_graph}, repository:" - f" {self._repository}" + if not query_string: + raise InvalidInputError( + f"Could not generate 'triples_update' query for triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" ) - else: + + result = self.query(query=query_string, update=True) + if not result: self.logger.warning( - f"Failed to update triples {old_triples} -> {new_triples}, named_graph: {self._named_graph}, repository:" - f" {self._repository}" + f"Failed to update triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" ) - return result + return False + + self.logger.debug( + f"Successfully updated triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + return True diff --git a/graph_db_interface/queries/triple_single.py b/graph_db_interface/queries/triple_single.py index 56a517f..0f5d16f 100644 --- a/graph_db_interface/queries/triple_single.py +++ b/graph_db_interface/queries/triple_single.py @@ -1,8 +1,12 @@ # To be imported into ..graph_db.py GraphDB class -from typing import Union, Any, Optional, TYPE_CHECKING -from rdflib import Literal +from typing import Optional, TYPE_CHECKING from graph_db_interface.utils import utils +from graph_db_interface.utils.iri import IRI +from graph_db_interface.utils.utils import ( + Triple, + PartialTriple, +) from graph_db_interface.exceptions import InvalidInputError from graph_db_interface.sparql_query import SPARQLQuery @@ -13,257 +17,204 @@ def triple_exists( self: "GraphDB", - sub: str, - pred: str, - obj: Union[str, Literal], + triple: Triple, + named_graph: Optional[IRI] = None, ) -> bool: """ - Checks if a specific triple exists in the graph database. + Check whether a specific triple exists in the graph database. Args: - sub (str): The subject of the triple. It will be processed to ensure it is an IRI. - pred (str): The predicate of the triple. It will be processed to ensure it is an IRI. - obj (Union[str, Literal]): The object of the triple. It can be a string or a Literal and - will be processed to ensure it is represented as a string. + triple (Triple): The triple `(subject, predicate, object)` to check. + named_graph (Optional[IRI]): Override the client's default named graph. Returns: - bool: True if the triple exists in the graph database, False otherwise. + bool: True if the triple exists, False otherwise. """ - sub = utils.prepare_subject(sub, ensure_iri=True) - pred = utils.prepare_predicate(pred, ensure_iri=True) - obj = utils.prepare_object(obj, as_string=True) + triple = utils.sanitize_triple(triple) + named_graph = named_graph or self.named_graph - query = SPARQLQuery(named_graph=self._named_graph, prefixes=self._prefixes) + query = SPARQLQuery(named_graph=named_graph) query.add_ask_block( where_clauses=[ - f"{sub} {pred} {obj} .", + utils.triple_to_string(triple, "."), ], ) query_string = query.to_string() result = self.query(query=query_string) - if result is not None and result["boolean"]: - self.logger.debug(f"Found triple {sub}, {pred}, {obj}") - return True + if result is None or result["boolean"] is False: + self.logger.debug( + f"Unable to find triple ({utils.triple_to_string(triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + return False self.logger.debug( - f"Unable to find triple {sub}, {pred}, {obj}, named_graph:" - f" {self._named_graph}, repository: {self._repository}" + f"Found triple ({utils.triple_to_string(triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" ) - return False + return True def triple_add( self: "GraphDB", - sub: str, - pred: str, - obj: Any, - named_graph: Optional[str] = None, + triple: Triple, + named_graph: Optional[IRI] = None, ) -> bool: """ - Adds a triple (subject, predicate, object) to the graph database. - - This method prepares the subject, predicate, and object to ensure they are - in the correct format (e.g., IRI or string) and constructs a SPARQL query - to insert the triple into the specified named graph. + Add a triple to the graph database. Args: - sub (str): The subject of the triple. It will be processed to ensure it - is a valid IRI. - pred (str): The predicate of the triple. It will be processed to ensure - it is a valid IRI. - obj (Any): The object of the triple. It will be processed to ensure it - is represented as a string. + triple (Triple): The triple `(subject, predicate, object)` to insert. + named_graph (Optional[IRI]): Override the client's default named graph. Returns: - bool: True if the triple was successfully inserted into the graph - database, False otherwise. + bool: True if the triple was inserted, False otherwise. """ - sub = utils.prepare_subject(sub, ensure_iri=True) - pred = utils.prepare_predicate(pred, ensure_iri=True) - obj = utils.prepare_object(obj, as_string=True) + triple = utils.sanitize_triple(triple) - query = SPARQLQuery( - named_graph=self._named_graph, - prefixes=self._prefixes, - ) + query = SPARQLQuery(named_graph=named_graph or self.named_graph) query.add_insert_data_block( - triples=[(sub, pred, obj)], + triples=[triple], ) query_string = query.to_string() if query_string is None: return False - if named_graph: - old_named_graph = self._named_graph - self.named_graph = named_graph result = self.query(query=query_string, update=True) - if result: - self.logger.debug( - f"New triple inserted: {sub}, {pred}, {obj} named_graph:" - f" {self._named_graph}, repository: {self._repository}" + if not result: + self.logger.warning( + f"Failed to insert triple: ({utils.triple_to_string(triple)}) named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" ) + return False - if named_graph: - self.named_graph = old_named_graph - return result + self.logger.debug( + f"Successfully inserted triple: ({utils.triple_to_string(triple)}) named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + + return True def triple_delete( self: "GraphDB", - sub: str, - pred: str, - obj: Union[str, Literal], - check_exist: bool = True, + triple: Triple, + check_exist: Optional[bool] = True, + named_graph: Optional[IRI] = None, ) -> bool: - """Delete a single triple. A SPAQRL delete query will be successfull, even though the triple to delete does not exist in the first place. + """ + Delete a single triple. + + A SPARQL DELETE operation in GraphDB can be successful even if the triple + does not exist. When `check_exist=True`, the function verifies the triple is + present before attempting deletion. Args: - subject (str): valid subject IRI - predicate (str): valid predicate IRI - object (str): valid object IRI - named_graph (str, optional): The IRI of a named graph. Defaults to None. - check_exist (bool, optional): Flag if you want to check if the triple exists before aiming to delete it. Defaults to True. + triple (Triple): The triple `(subject, predicate, object)` to delete. + check_exist (Optional[bool]): Whether to verify existence prior to deletion. Defaults to True. + named_graph (Optional[IRI]): Override the client's default named graph. Returns: - bool: Returns True if query was successfull. False otherwise. + bool: True if deletion succeeded (or triple absent when `check_exist=False`), False otherwise. """ - sub = utils.prepare_subject(sub, ensure_iri=True) - pred = utils.prepare_predicate(pred, ensure_iri=True) - obj = utils.prepare_object(obj, as_string=True) + triple = utils.sanitize_triple(triple) if check_exist: - if not self.triple_exists(sub, pred, obj): + if not self.triple_exists(triple): self.logger.warning("Unable to delete triple since it does not exist") return False - query = SPARQLQuery( - named_graph=self._named_graph, - prefixes=self._prefixes, - ) + query = SPARQLQuery(named_graph=named_graph or self.named_graph) query.add_delete_data_block( - triples=[(sub, pred, obj)], + triples=[triple], ) query_string = query.to_string() if query_string is None: return False - # Execute the SPARQL query result = self.query(query=query_string, update=True) - if result: - self.logger.debug(f"Successfully deleted triple: {sub} {pred} {obj}") - else: - self.logger.warning(f"Failed to delete triple: {sub} {pred} {obj}") + if not result: + self.logger.warning( + f"Failed to delete triple: ({utils.triple_to_string(triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + return False - return result + self.logger.debug( + f"Successfully deleted triple: ({utils.triple_to_string(triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + return True def triple_update( self: "GraphDB", - sub_old: str, - pred_old: str, - obj_old: Union[str, Literal], - sub_new: Optional[str] = None, - pred_new: Optional[str] = None, - obj_new: Optional[Union[str, Literal]] = None, - check_exist: bool = True, + old_triple: Triple, + new_triple: Optional[PartialTriple] = None, + new_sub: Optional[IRI] = None, + new_pred: Optional[IRI] = None, + new_obj: Optional[IRI] = None, + check_exist: Optional[bool] = True, + named_graph: Optional[IRI] = None, ) -> bool: """ - Updates any part of an existing triple (subject, predicate, or object) in the RDF store. + Update a triple by replacing any of its parts. - This function replaces the specified part of an existing triple using a SPARQL - `DELETE ... INSERT ... WHERE` query. + Performs a SPARQL `DELETE ... INSERT ... WHERE` that replaces the old triple + with a new triple built from provided parts. Args: - old_subject (str, optional): The subject of the triple to be updated. - old_predicate (str, optional): The predicate of the triple to be updated. - old_object (str, optional): The object of the triple to be updated. - new_subject (str, optional): The new subject to replace the old subject. - new_predicate (str, optional): The new predicate to replace the old predicate. - new_object (str, optional): The new object to replace the old object. - named_graph (str, optional): The named graph where the triple update should be performed. - check_exist (bool, optional): If `True`, checks if the old triple exists before updating. - Defaults to `True`. + old_triple (Triple): Existing triple to update. + new_triple (Optional[PartialTriple]): Replacement values (subject/predicate/object). Use this + or `new_sub`/`new_pred`/`new_obj`. + new_sub (Optional[IRI]): Replacement subject. + new_pred (Optional[IRI]): Replacement predicate. + new_obj (Optional[IRI]): Replacement object. + check_exist (Optional[bool]): If True, verify that `old_triple` exists before updating. Defaults to True. + named_graph (Optional[IRI]): Override the client's default named graph. Returns: - bool: `True` if the update was successful, `False` otherwise. + bool: True if the update succeeded, False otherwise. Raises: - Any exceptions thrown by `self.query()` if the SPARQL update request fails. - - Example: - ```python - success = rdf_store.triple_update_any( - old_subject="", - old_predicate="", - old_object="", - new_subject="" - ) - ``` + InvalidInputError: If neither or both of `new_triple` and any of `new_sub`/`new_pred`/`new_obj` are provided, + or if input triples are incomplete/invalid. """ - if not (sub_old and pred_old and obj_old): + elems_given = new_sub is not None or new_pred is not None or new_obj is not None + if (new_triple and elems_given) or (not new_triple and not elems_given): raise InvalidInputError( - "All parts of the triple to update (sub_old, pred_old, obj_old) must be provided." + "Either 'new triple' or 'new_sub/new_pred/new_obj' must be provided, not both." ) - if sub_new is None and pred_new is None and obj_new is None: - raise InvalidInputError( - "At least one of sub_new, pred_new, or obj_new must be provided." - ) - - sub_old = utils.prepare_subject(sub_old, ensure_iri=True) - pred_old = utils.prepare_predicate(pred_old, ensure_iri=True) - obj_old = utils.prepare_object(obj_old, as_string=True) + old_triple = utils.sanitize_triple(old_triple) + new_triple = utils.sanitize_triple( + new_triple or (new_sub, new_pred, new_obj), + allow_partial=True, + ) if check_exist: - if not self.triple_exists( - sub_old, - pred_old, - obj_old, - ): + if not self.triple_exists(old_triple): self.logger.warning( - f"Triple does not exist: {sub_old} {pred_old} {obj_old}" + f"Triple does not exist: ({utils.triple_to_string(old_triple)})" ) return False - if sub_new is not None: - sub_new = utils.prepare_subject(sub_new, ensure_iri=True) - if pred_new is not None: - pred_new = utils.prepare_predicate(pred_new, ensure_iri=True) - if obj_new is not None: - obj_new = utils.prepare_object(obj_new, as_string=True) - # Determine replacement variables - update_sub = sub_new if sub_new else sub_old - update_pred = pred_new if pred_new else pred_old - update_obj = obj_new if obj_new else obj_old + update_triple = tuple(n if n else o for o, n in zip(old_triple, new_triple)) - query = SPARQLQuery( - named_graph=self._named_graph, - prefixes=self._prefixes, - ) + query = SPARQLQuery(named_graph=named_graph or self.named_graph) query.add_delete_insert_data_block( - delete_triples=[(sub_old, pred_old, obj_old)], - insert_triples=[(update_sub, update_pred, update_obj)], - where_clauses=[f"{sub_old} {pred_old} {obj_old} ."], + delete_triples=[old_triple], + insert_triples=[update_triple], + where_clauses=[utils.triple_to_string(old_triple, ".")], ) query_string = query.to_string(validate=True) if query_string is None: return False result = self.query(query=query_string, update=True) - - if result: - self.logger.debug( - f"Successfully updated triple to: {update_sub} {update_pred}" - f" {update_obj}, named_graph: {self._named_graph}, repository:" - f" {self._repository}" - ) - else: + if not result: self.logger.warning( - f"Failed to update triple to: {update_sub} {update_pred}" - f" {update_obj}, named_graph: {self._named_graph}, repository:" - f" {self._repository}" + f"Failed to update triple to: ({utils.triple_to_string(update_triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" ) + return False - return result + self.logger.debug( + f"Successfully updated triple to: ({utils.triple_to_string(update_triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + ) + return True diff --git a/graph_db_interface/sparql_query.py b/graph_db_interface/sparql_query.py index bf5ce5f..9cba61c 100644 --- a/graph_db_interface/sparql_query.py +++ b/graph_db_interface/sparql_query.py @@ -1,6 +1,8 @@ from enum import Enum -from typing import List, Optional, Dict, Tuple +from typing import List, Optional from graph_db_interface.utils import utils +from graph_db_interface.utils.utils import Triple +from graph_db_interface.utils.iri import IRI class SPARQLQueryType(Enum): @@ -19,15 +21,20 @@ class SPARQLQueryType(Enum): class SPARQLQuery: + """ + Helper for composing and validating SPARQL queries. + + Builds SELECT/ASK and UPDATE blocks (INSERT/DELETE) with optional default + named graph and control over explicit/implicit inference. + """ + def __init__( self, - named_graph: Optional[str] = None, - prefixes: Optional[Dict[str, str]] = None, - include_explicit: bool = True, - include_implicit: bool = True, + named_graph: Optional[IRI] = None, + include_explicit: Optional[bool] = True, + include_implicit: Optional[bool] = True, ): self._named_graph = named_graph - self._prefixes = prefixes self._include_explicit = include_explicit self._include_implicit = include_implicit self._query_blocks = [] @@ -36,15 +43,23 @@ def add_select_block( self, variables: List[str], where_clauses: List[str], - select_type: SPARQLQueryType = SPARQLQueryType.SELECT, - ) -> str: + select_type: Optional[SPARQLQueryType] = SPARQLQueryType.SELECT, + ) -> None: + """ + Add a SELECT block to the query. + + Args: + variables (List[str]): Variable names to project (e.g., ["?s", "?p", "?o"]). + where_clauses (List[str]): WHERE patterns/filters to include. + select_type (Optional[SPARQLQueryType]): SELECT variant (e.g., DISTINCT). Defaults to SELECT. + """ block_parts = [] block_parts.append( f"{select_type.value} {self._create_variable_string(variables)}" ) part = self._add_explicit_implicit() if self._named_graph: - block_parts.append(f"FROM {utils.ensure_absolute(self._named_graph)}") + block_parts.append(f"FROM {self._named_graph.n3()}") if part: block_parts.append(part) block_parts.append(f"WHERE {{{self._combine_where_clauses(where_clauses)}}}") @@ -54,7 +69,13 @@ def add_select_block( def add_ask_block( self, where_clauses: List[str], - ) -> str: + ) -> None: + """ + Add an ASK block to the query. + + Args: + where_clauses (List[str]): WHERE patterns/filters to include. + """ block_parts = [] block_parts.append("ASK") part = self._add_explicit_implicit() @@ -72,11 +93,18 @@ def add_ask_block( def add_insert_data_block( self, - triples: List[Tuple[str]], - ) -> str: + triples: List[Triple], + ) -> None: + """ + Add an INSERT DATA block comprised of triples. + + Args: + triples (List[Triple]): Triples to insert. + """ block_parts = [] data_combined = "\n".join( - f"{triple[0]} {triple[1]} {triple[2]} ." for triple in triples + utils.triple_to_string(utils.sanitize_triple(triple), ".") + for triple in triples ) block_parts.append( f"""INSERT DATA {{ @@ -89,11 +117,18 @@ def add_insert_data_block( def add_insert_exists_block( self, - triples: List[Tuple[str]], - ) -> str: + triples: List[Triple], + ) -> None: + """ + Add an INSERT ... WHERE NOT EXISTS block. + + Args: + triples (List[Triple]): Triples to insert when they do not already exist. + """ block_parts = [] data_combined = "\n".join( - f"{triple[0]} {triple[1]} {triple[2]} ." for triple in triples + utils.triple_to_string(utils.sanitize_triple(triple), ".") + for triple in triples ) block_parts.append( f"""INSERT {{ @@ -111,11 +146,18 @@ def add_insert_exists_block( def add_delete_data_block( self, - triples: List[Tuple[str]], - ) -> str: + triples: List[Triple], + ) -> None: + """ + Add a DELETE DATA block comprised of triples. + + Args: + triples (List[Triple]): Triples to delete. + """ block_parts = [] data_combined = "\n".join( - f"{triple[0]} {triple[1]} {triple[2]} ." for triple in triples + utils.triple_to_string(utils.sanitize_triple(triple), ".") + for triple in triples ) block_parts.append( f"""DELETE DATA {{ @@ -128,20 +170,30 @@ def add_delete_data_block( def add_delete_insert_data_block( self, - delete_triples: List[Tuple[str]], - insert_triples: List[Tuple[str]], + delete_triples: List[Triple], + insert_triples: List[Triple], where_clauses: List[str], - ): + ) -> None: + """ + Add a combined DELETE/INSERT block with WHERE. + + Args: + delete_triples (List[Triple]): Triples to delete. + insert_triples (List[Triple]): Triples to insert. + where_clauses (List[str]): WHERE patterns selecting the triples to update. + """ block_parts = [] if self._named_graph: - block_parts.append(f"WITH {utils.ensure_absolute(self._named_graph)}") + block_parts.append(f"WITH {self._named_graph.n3()}") delete_triples_combined = "\n".join( - f"{triple[0]} {triple[1]} {triple[2]} ." for triple in delete_triples + utils.triple_to_string(utils.sanitize_triple(triple), ".") + for triple in delete_triples ) block_parts.append(f"DELETE {{{delete_triples_combined}}}") insert_triples_combined = "\n".join( - f"{triple[0]} {triple[1]} {triple[2]} ." for triple in insert_triples + utils.triple_to_string(utils.sanitize_triple(triple), ".") + for triple in insert_triples ) block_parts.append(f"INSERT {{{insert_triples_combined}}}") @@ -151,36 +203,69 @@ def add_delete_insert_data_block( {"type": SPARQLQueryType.DELETE_INSERT, "data": block} ) - def _create_variable_string(self, variables: List[str]) -> str: - """Create a string representation of the variables for the SELECT query.""" + def _create_variable_string( + self, + variables: List[str], + ) -> str: + """ + Create a string representation of projected variables. + + Args: + variables (List[str]): Variables to project. + + Returns: + str: Space-separated variables or "*" if empty. + """ return " ".join(variables) if variables else "*" - def _combine_where_clauses(self, where_clauses: List[str]) -> str: + def _combine_where_clauses( + self, + where_clauses: List[str], + ) -> str: + """ + Combine WHERE clauses into a single string separated by newlines. + + Args: + where_clauses (List[str]): WHERE clause strings. + + Returns: + str: Combined WHERE content. + """ if len(where_clauses) >= 1: return "\n".join(where_clauses) else: return "" - def _get_prefix_string(self) -> str: - return ( - "\n".join( - f"PREFIX {prefix}: {iri}" for prefix, iri in self._prefixes.items() - ) - + "\n" - ) - def _add_explicit_implicit(self) -> Optional[str]: + """ + Generate a FROM clause for explicit/implicit inclusion. + + Returns: + Optional[str]: A `FROM <...>` clause or `None` when both are included. + """ if self._include_explicit and not self._include_implicit: - return "FROM onto:explicit" + return f"FROM " elif self._include_implicit and not self._include_explicit: - return "FROM onto:implicit" + return f"FROM " return None - def to_string(self, validate: bool = True) -> str: - query_parts = [] - if self._prefixes: - query_parts.append(self._get_prefix_string()) + def to_string( + self, + validate: Optional[bool] = True, + ) -> str: + """ + Compose the full SPARQL string and optionally validate it. + + Args: + validate (Optional[bool]): If True, validate the query or update structure. + Returns: + str: The composed query string. + + Raises: + InvalidQueryError: If validation of the composed query fails. + """ + query_parts = [] for block in self._query_blocks: query_parts.append(block["data"]) diff --git a/graph_db_interface/utils/graph_db_credentials.py b/graph_db_interface/utils/graph_db_credentials.py index 81a3217..33d7da4 100644 --- a/graph_db_interface/utils/graph_db_credentials.py +++ b/graph_db_interface/utils/graph_db_credentials.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass import os @@ -5,14 +7,16 @@ @dataclass(frozen=True) class GraphDBCredentials: """ - A class representing database credentials for connecting to a graph database. + Immutable credentials for connecting to a GraphDB instance. - Attributes: - host (str): The hostname or IP address of the database server. - username (str): The username for authenticating with the database. - password (str): The password for authenticating with the database. - database_name (str): The name of the specific database to connect to. + Contains the base URL, user credentials, and target repository used by + the client to authenticate and perform SPARQL operations. + Args: + base_url (str): Base URL of the GraphDB instance (e.g., "http://localhost:7200"). + username (str): Username for GraphDB authentication. + password (str): Password for GraphDB authentication. + repository (str): Repository identifier to target within the instance. """ base_url: str @@ -21,16 +25,18 @@ class GraphDBCredentials: repository: str @classmethod - def from_env(cls): + def from_env(cls) -> GraphDBCredentials: """ - Create a GraphDB instance using environment variables. The following environment variables must be set: - - `GRAPHDB_USERNAME`: The username for GraphDB authentication. - - `GRAPHDB_PASSWORD`: The password for GraphDB authentication. - - `GRAPHDB_URL`: The base URL of the GraphDB instance. - - `GRAPHDB_REPOSITORY`: The name of the GraphDB repository to use. + Build credentials from environment variables. + + The following environment variables must be set: `GRAPHDB_USERNAME`, + `GRAPHDB_PASSWORD`, `GRAPHDB_URL`, and `GRAPHDB_REPOSITORY`. + + Returns: + GraphDBCredentials: A credentials instance populated from the environment. Raises: - ValueError: If any of the required environment variables are not set. + ValueError: If any of the required environment variables are missing. """ if os.getenv("GRAPHDB_USERNAME") is None: @@ -53,13 +59,3 @@ def from_env(cls): password=password, repository=repository, ) - - def __iter__(self): - return iter( - ( - self.base_url, - self.username, - self.password, - self.repository, - ) - ) diff --git a/graph_db_interface/utils/iri.py b/graph_db_interface/utils/iri.py new file mode 100644 index 0000000..e519a62 --- /dev/null +++ b/graph_db_interface/utils/iri.py @@ -0,0 +1,292 @@ +from __future__ import annotations + +from graph_db_interface.exceptions import InvalidIRIError + +from typing import Optional, Any, Dict +from rdflib import URIRef, Literal +from pydantic import GetCoreSchemaHandler +from pydantic_core import CoreSchema, core_schema + + +class IRI(URIRef): + """ + Lightweight IRI wrapper with prefix support and validation. + + The stored format is a full IRI string, eg. `http://example.org#Name`. + Prefixes are resolved from the provided dictionary or class-level registry. + + Valid input formats are: + - Full IRIs without fragments: `http://example.org` + - Prefixes without fragments: `prefix:` + - Full IRI with fragment: `http://example.org#Name` + - Prefixes with fragments: `prefix:Name` + + Validation supports: + - `str`, `rdflib.URIRef`, `IRI` or other subclass of `str` inputs. + `rdflib.Literal` or non-string inputs raise `TypeError`. + If desired, convert to string explicitly before passing. + - Separate base and name: `base="http://example.org", value="Name"`. + - Angle brackets `<...>`. + - Trailing `#`, `\` or `:` from value and base. + """ + + PREFIXES = { + "owl": "http://www.w3.org/2002/07/owl", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns", + "rdfs": "http://www.w3.org/2000/01/rdf-schema", + "kafka": "http://www.ontotext.com/connectors/kafka", + "kafka-inst": "http://www.ontotext.com/connectors/kafka/instance", + } + PREFIXES_INV = {v: k for k, v in PREFIXES.items()} + + # Valid schemes for full IRIs, end with '://' + SCHEMES = {"http://", "https://"} + + def __new__( + cls, + value: str, + base: Optional[str] = None, + prefixes: Optional[Dict[str, str]] = None, + ) -> IRI: + """ + Create a new IRI instance with normalization and validation. + + Args: + value (str): Full IRI, shorthand (`prefix:name`), or local name. + base (Optional[str]): Optional base IRI or prefix to combine with `value`. + prefixes (Optional[Dict[str, str]]): Additional prefix mappings to use. + + Returns: + IRI: The normalized IRI instance. + + Raises: + TypeError: If inputs are not strings or are of type `rdflib.Literal`. + If desired, convert to string explicitly before passing. + InvalidIRIError: If inputs cannot be resolved into a valid IRI. + """ + iri = IRI._sanitize(value, base, prefixes) + return super().__new__(cls, iri) + + def short( + self, + ) -> str: + """ + Return a prefixed form if the base is registered. + + Returns: + str: `prefix:name` if the base IRI is known; otherwise the full IRI. + """ + onto, fragment = str(self).rsplit("#", 1) + if onto in IRI.PREFIXES_INV: + return f"{IRI.PREFIXES_INV[onto]}:{fragment}" + return str(self) + + def __hash__(self) -> int: + """ + Hash based on the string form to ensure stable behavior in sets/dicts. + + Returns: + int: The hash value. + """ + return str(self).__hash__() + + def __eq__( + self, + other: Any, + ) -> bool: + """ + Compare IRIs by string value with tolerant string inputs. + + Args: + other (Any): Another `IRI`, `URIRef`, or string to compare. + + Returns: + bool: True if the IRIs resolve to the same string form. + """ + # Includes IRI + if isinstance(other, URIRef): + return str(self) == str(other) + # Sanitize string before comparison + if isinstance(other, str): + try: + return self == IRI(other) + except: + return False + return False + + @classmethod + def add_prefix( + cls, + prefix: str, + iri: str, + ) -> None: + """ + Register or overwrite a prefix mapping. + + Args: + prefix (str): Prefix label (e.g., "ex"). + iri (str): Base IRI to map (with or without angle brackets or trailing '#'). + """ + iri = str(IRI(iri)) + cls.PREFIXES[prefix] = iri + cls.PREFIXES_INV[iri] = prefix + + @classmethod + def remove_prefix( + cls, + prefix: str, + ) -> bool: + """ + Remove a registered prefix mapping. + + Args: + prefix (str): The prefix label to remove. + + Returns: + bool: True if the prefix existed and was removed, False otherwise. + """ + if prefix in cls.PREFIXES: + del cls.PREFIXES_INV[cls.PREFIXES[prefix]] + del cls.PREFIXES[prefix] + return True + return False + + @classmethod + def get_prefixes(cls) -> Dict[str, str]: + """ + Get a copy of the current prefix registry. + + Returns: + Dict[str, str]: Mapping of prefix to base IRI (without trailing '#'). + """ + return cls.PREFIXES.copy() + + @classmethod + def _sanitize( + cls, + value: str, + base: Optional[str], + prefixes: Optional[Dict[str, str]] = None, + ) -> str: + """ + Normalize and validate IRI inputs into a canonical string form. + + Args: + value (str): Full IRI, shorthand (`prefix:name`), or local name. + base (Optional[str]): Base IRI or prefix to combine with `value`. + prefixes (Optional[Dict[str, str]]): Additional prefix mappings to use. + + Returns: + str: The normalized IRI string. + + Raises: + TypeError: If inputs are not strings or are of type `rdflib.Literal`. + If desired, convert to string explicitly before passing. + InvalidIRIError: If inputs cannot be resolved into a valid IRI. + """ + if value is None and base is None: + raise InvalidIRIError("Invalid IRI: both value and base cannot be None") + + # Shortcut known good IRIs + if isinstance(value, IRI) and base is None: + return str(value) + + if isinstance(base, IRI) and value is None: + return str(base) + + # Convert value to str + if value is not None: + if isinstance(value, Literal): + raise TypeError( + f"'value' is of type Literal and probably not intended as IRI. Convert to string explicitly if intended: {value}" + ) + if not isinstance(value, str): + raise TypeError( + f"Invalid IRI: value is not a string and probably not intended as IRI. Convert to string explicitly if intended: {value}" + ) + value = str(value) + + if base is not None: + if isinstance(base, Literal): + raise TypeError( + f"'base' is of type Literal and probably not intended as IRI. Convert to string explicitly if intended: {base}" + ) + if not isinstance(base, str): + raise TypeError( + f"Invalid IRI: base is not a string and probably not intended as IRI. Convert to string explicitly if intended: {base}" + ) + base = str(base) + + # Merge base and value + raw_base = base.strip("<>").rstrip("#:/") if base is not None else "" + raw_value = value.strip("<>").rstrip("#:/") if value is not None else "" + if raw_base == "" and raw_value == "": + raise InvalidIRIError("Invalid IRI: empty string") + + if raw_base == "": + raw = raw_value + elif raw_value == "": + raw = raw_base + elif raw_base in IRI.PREFIXES.keys(): + raw = raw_base + ":" + raw_value + else: + raw = raw_base + "#" + raw_value + + # Resolve prefixes and formats + prefixes = IRI.PREFIXES if prefixes is None else {**IRI.PREFIXES, **prefixes} + + # Direct prefix, without fragment + if raw in prefixes: + return prefixes[raw] + + # Full IRI + if any(raw.startswith(scheme) for scheme in IRI.SCHEMES): + if raw.count(":") > 1: + raise InvalidIRIError( + f"Invalid IRI: ':' outside of supported schemes {IRI.SCHEMES} ({value}, {base})" + ) + if raw.count("#") > 1: + raise InvalidIRIError(f"Invalid IRI: multiple '#' in ({value}, {base})") + return raw + + # Mixed or malformed combinations (e.g., 'owl#Class', 'owl:owl#Class', 'owl#owl:Class') + if ":" in raw and "#" in raw: + raise InvalidIRIError( + f"Invalid IRI: mixed '#' and ':' outside of supported schemes {IRI.SCHEMES} in ({value}, {base})" + ) + if "#" in raw: + raise InvalidIRIError(f"Invalid IRI: malformed format ({value}, {base})") + + colon_count = raw.count(":") + if colon_count > 1: + raise InvalidIRIError( + f"Invalid IRI: multiple ':' separators in ({value}, {base})" + ) + if colon_count == 1: + # One ':': 'prefix:fragment' form + prefix, fragment = raw.split(":") + if prefix not in prefixes: + raise InvalidIRIError(f"Invalid IRI: unknown prefix '{prefix}'") + return prefixes[prefix] + "#" + fragment + + raise InvalidIRIError( + f"Invalid IRI: malformed format or unknown prefix ({value}, {base})" + ) + + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + """ + Provide a permissive Pydantic core schema for IRI fields. + + Args: + source_type (Any): The source type passed by Pydantic. + handler (GetCoreSchemaHandler): Pydantic schema handler. + + Returns: + CoreSchema: A schema accepting any value (validated by IRI itself). + """ + return core_schema.any_schema() diff --git a/graph_db_interface/utils/pretty_print.py b/graph_db_interface/utils/pretty_print.py index c868725..7c35408 100644 --- a/graph_db_interface/utils/pretty_print.py +++ b/graph_db_interface/utils/pretty_print.py @@ -10,34 +10,35 @@ def format_result( variables: Optional[list[str]] = None, grouping_variables: Optional[list[str]] = None, ) -> str: - """Return a human-readable string for SPARQL query results. + """ + Return a human-readable string for SPARQL query results. This utility formats different shapes of results produced by SPARQL SELECT queries into an easy-to-read textual representation used in logs or CLI output. - Parameters: - result: One of the following structures: + Args: + result (list[tuple[str]] | tuple[tuple[str, ...], ...] | dict[Any, Any] | list[dict]): + One of the following structures: - tuple of tuples: tabular rows, where each inner tuple corresponds to a row - ordered by ``variables``. - - tuple of scalars: a single-column result (when ``variables`` has length 1). - - dict: a nested mapping keyed by the values of ``grouping_variables`` where the leaves - are tuples (or empty tuples) representing the non-grouped ``variables``. + ordered by `variables`. + - tuple of scalars: a single-column result (when `variables` has length 1). + - dict: a nested mapping keyed by the values of `grouping_variables` where the leaves + are tuples (or empty tuples) representing the non-grouped `variables`. - list[dict]: raw JSON bindings from a SPARQL endpoint (each dict is a binding); this is rendered as a compact pretty-printed JSON-like list. - variables: List of non-grouped variable names (without leading ``?``) that define the - order and width of columns when rendering tabular/tuple results. May be ``None`` when - rendering grouped structures only. - grouping_variables: List of grouped variable names (without leading ``?``) that define the - nesting order for dictionary results. When present, headers are annotated by the current - grouping key at each level. + variables (Optional[list[str]]): Non-grouped variable names (without leading `?`) that + define the order and width of columns when rendering tabular/tuple results. + grouping_variables (Optional[list[str]]): Grouped variable names (without leading `?`) that + define the nesting order for dictionary results. When present, headers are annotated by + the current grouping key at each level. Returns: - A formatted multi-line string. For empty inputs the string explicitly indicates which + str: A formatted multi-line string. For empty inputs the string explicitly indicates which variables and/or grouping variables were expected. Notes: - This function does not mutate the input data. - - Widths for tabular output are computed from both headers (``variables``) and the values. + - Widths for tabular output are computed from both headers (`variables`) and the values. """ if not result: @@ -62,14 +63,17 @@ def format_result( return f"Result: {_format_entry(result, variables)}" -def _format_raw_json_bindings(result: list[dict]) -> str: - """Format raw JSON bindings (list of dicts) as pretty-printed JSON. +def _format_raw_json_bindings( + result: list[dict], +) -> str: + """ + Format raw JSON bindings (list of dicts) as pretty-printed JSON. - Parameters: - result: A list of dictionaries representing SPARQL query bindings. + Args: + result (list[dict]): A list of dictionaries representing SPARQL query bindings. Returns: - A formatted JSON string representation of the input list. + str: A formatted JSON string representation of the input list. """ try: return json.dumps(result, indent=2, ensure_ascii=False) @@ -84,21 +88,22 @@ def _format_raw_json_bindings(result: list[dict]) -> str: def _format_nested_structure( structure, - variables: Optional[list[str]], - grouping_variables: Optional[list[str]], - level: int = 0, + variables: list[str], + grouping_variables: list[str], + level: Optional[int] = 0, ) -> str: - """Format a nested dictionary result structure with indentation. + """ + Format a nested dictionary result structure with indentation. - Parameters: - structure: A nested dict keyed by the values of ``grouping_variables`` where leaves are - tuples (or empty tuples) representing the non-grouped ``variables``. - variables: The non-grouped variable names used to render leaf tuples. - grouping_variables: Grouped variable names used to annotate each nesting level. - level: Current recursion depth used only for indentation. + Args: + structure (dict): Nested dict keyed by the values of `grouping_variables` where leaves are + tuples (or empty tuples) representing the non-grouped `variables`. + variables (list[str]): The non-grouped variable names used to render leaf tuples. + grouping_variables (list[str]): Grouped variable names used to annotate each nesting level. + level (Optional[int]): Current recursion depth used only for indentation. Returns: - A multi-line, indented string representation of the nested structure. + str: A multi-line, indented string representation of the nested structure. """ indent = " " * level if not structure: @@ -118,18 +123,21 @@ def _format_nested_structure( return f"\n {_format_entry(structure, variables, indent[:-1])}" -def _format_entry(structure: tuple, variables: list[str], indent: str = "") -> str: - """Format a flat tuple or tuple-of-tuples as aligned columns. +def _format_entry( + structure: tuple, + variables: list[str], + indent: Optional[str] = "", +) -> str: + """ + Format a flat tuple or tuple-of-tuples as aligned columns. - Parameters: - structure: Either a tuple of scalars (single-column) or a tuple of tuples (multi-column). - variables: Column headers (names without leading ``?``) used for width computation - and header rendering. - indent: Optional left indentation for multi-line alignment. + Args: + structure (tuple): Either a tuple of scalars (single-column) or a tuple of tuples (multi-column). + variables (list[str]): Column headers (without leading `?`) used for width computation and header rendering. + indent (Optional[str]): Optional left indentation for multi-line alignment. Returns: - A string with a header row and aligned values. Single-column outputs render as a - vertical list; multi-column outputs render as a table-like block. + str: A header row and aligned values. Single-column outputs render as a vertical list; multi-column outputs render as a table-like block. """ if len(variables) == 1: lmax = len(variables[0]) diff --git a/graph_db_interface/utils/processing.py b/graph_db_interface/utils/processing.py index 795e285..7df54b3 100644 --- a/graph_db_interface/utils/processing.py +++ b/graph_db_interface/utils/processing.py @@ -6,74 +6,38 @@ def process_bindings_select( variables: Optional[list[str]] = None, grouping_variables: Optional[list[str]] = None, ) -> tuple[tuple[Any, ...], ...] | tuple[Any, ...] | dict: - """Process SPARQL bindings from a SELECT query into tuples or a nested dictionary structure. + """ + Process SPARQL SELECT bindings into tuples or a nested dictionary. - This helper takes the raw SPARQL JSON bindings (``results.bindings``) and - converts them into a convenient Python structure. + Converts raw SPARQL JSON bindings (`results.bindings`) into a convenient Python + structure for downstream processing and display. - Parameters: - bindings (list[dict]): Raw bindings from SPARQL query results - (i.e., ``response['results']['bindings']``). - variables (Optional[list[str]]): Variable names (without leading ``?``) - that define the tuple entries and their order in the result. - grouping_variables (Optional[list[str]]): Grouping variable names (without leading ``?``) + Args: + bindings (list[dict[str, dict[str, Any]]]): Raw bindings from SPARQL query results + (i.e., `response['results']['bindings']`). + variables (Optional[list[str]]): Variable names (without leading `?`) that define the + tuple entries and their order in the result. + grouping_variables (Optional[list[str]]): Grouping variable names (without leading `?`) that define hierarchical dictionary keys in order. When provided, the result is a - nested dict keyed by the values of these variables, with leaves as described below. + nested dict keyed by these values, with leaves as described below. Returns: - tuple[tuple[Any, ...], ...] | tuple[Any, ...] | dict: - When grouping_variables is not provided: - - variables is empty or None -> () (empty tuple) - - variables has length 1 -> (a1, a2, ...) (tuple of scalar values) - - variables has length > 1 -> ((a1, b1, ...), (a2, b2, ...), ...) (tuple of tuples) - When grouping_variables is provided: - - Nested dict keyed by the values of grouping_variables (in order) - - Each leaf is processed using the entries in variables (shapes as above) + tuple[tuple[Any, ...], ...] | tuple[Any, ...] | dict: When `grouping_variables` is not provided: + - variables is empty or None -> () (empty tuple) + - variables has length 1 -> (a1, a2, ...) (tuple of scalar values) + - variables has length > 1 -> ((a1, b1, ...), (a2, b2, ...), ...) (tuple of tuples) + When `grouping_variables` is provided: + - Nested dict keyed by the values of `grouping_variables` (in order) + - Each leaf is processed using the entries in `variables` (shapes as above) Raises: - AssertionError: If any of the values referenced by ``grouping_variables`` in the - first binding are not hashable. Keys in ``grouping_variables`` must map to hashable - values to be usable as dictionary keys. - TypeError: If the bindings are empty and neither ``variables`` nor ``grouping_variables`` are provided. + AssertionError: If any values referenced by `grouping_variables` in the first binding + are not hashable. Grouping keys must be hashable to serve as dict keys. + TypeError: If the bindings are empty and neither `variables` nor `grouping_variables` are provided. Notes: - - ``variables`` and ``grouping_variables`` are expected without the leading ``?``. - - If neither ``variables`` nor ``grouping_variables`` are provided, the ``variables`` are inferred from the bindings. - - Example: - 1) Without grouping: - ``` - grouping_variables = None - variables = ["?machine", "?part", "?parameter", "?value"] - result -> ( - ("machine1", "part1", "parameter1", "value1"), - ("machine1", "part1", "parameter2", "value2"), - ("machine1", "part2", "parameter1", "value3"), - ("machine1", "part2", "parameter2", "value4"), - ... - ) - ``` - 2) With grouping: - ``` - grouping_variables = ["?machine", "?part"] - variables = ["?parameter", "?value"] - result -> { - "machine1": { - "part1": ( - ("parameter1", "value1"), - ("parameter2", "value2"), - ... - ), - "part2": ( - ("parameter1", "value3"), - ("parameter2", "value4"), - ... - ), - ... - }, - ... - } - ``` + - `variables` and `grouping_variables` are expected without the leading `?`. + - If neither is provided, `variables` are inferred from the first binding when available. """ if not variables and not grouping_variables: diff --git a/graph_db_interface/utils/utils.py b/graph_db_interface/utils/utils.py index f143faa..54a1005 100644 --- a/graph_db_interface/utils/utils.py +++ b/graph_db_interface/utils/utils.py @@ -1,19 +1,128 @@ import logging -from typing import Union, Dict, Optional, Any -from rdflib import URIRef, Literal, XSD, Dataset +from typing import TypeAlias, Union, Optional, Any, Tuple +from rdflib import Literal, XSD, Dataset from rdflib.plugins.sparql.processor import prepareQuery -from urllib.parse import urlparse from graph_db_interface.exceptions import ( InvalidInputError, - InvalidIRIError, InvalidQueryError, ) +from graph_db_interface.utils.iri import IRI LOGGER = logging.getLogger(__name__) +Triple: TypeAlias = Tuple[ + Union[str, IRI], + Union[str, IRI], + Union[str, IRI, Literal], +] + +PartialTriple: TypeAlias = Tuple[ + Optional[Union[str, IRI]], + Optional[Union[str, IRI]], + Optional[Union[str, IRI, Literal]], +] + + +def sanitize_triple( + triple: Triple, + allow_partial: Optional[bool] = False, +) -> Triple: + """ + Validates and converts the components of a triple to their appropriate types. + + - A valid triple must have three entries (subject, predicate, object) defined. + - If allow_partial is false, no entry may be None. Otherweise, at least one entry + must be not None. + - Subject and predicate must be valid IRIs. + - The object can be either an IRI or a Literal + - If the object is of type IRI, URIRef, or str, it will be converted to IRI. + - If an object of type str should be treated as a Literal, it must be + explicitly converted to Literal before passing. + - If the object is of any other type, it will be converted to a Literal. + + Args: + triple (Triple): The triple to validate and convert. + allow_partial (Optional[bool]): Whether to allow partial triples. Defaults to False. + + Returns: + Triple: The validated and converted triple. + + Raises: + InvalidInputError: If the triple does not have three or too many None entries. + TypeError: If subject or predicate are not of type str, URIRef, or IRI. + InvalidIRIError: If object is of type str or URIRef (except Literal) and not in a valid IRI format. + """ + + if allow_partial: + if len(triple) != 3 or all(e is None for e in triple): + error_message = f"Triple requires three components, at least one of which is not None: {triple}" + LOGGER.error(error_message) + raise InvalidInputError(error_message) + else: + if len(triple) != 3 or any(e is None for e in triple): + error_message = ( + f"Triple requires three components, neither of which is None: {triple}" + ) + LOGGER.error(error_message) + raise InvalidInputError(error_message) + + sub, pred, obj = triple + + # Sub must be IRI + if sub is not None and not isinstance(sub, IRI): + sub = IRI(sub) + + # Pred must be IRI + if pred is not None and not isinstance(pred, IRI): + pred = IRI(pred) + + # Pred must be IRI or Literal + # If str, try IRI first, then Literal. Otherwise, Literal. + if obj is not None and not isinstance(obj, (IRI, Literal)): + if isinstance(obj, str): + try: + obj = IRI(str(obj)) + except Exception as e: + error_message = ( + "Object is of type str but cannot be converted to IRI. If object " + + f"is a , explicitly convert before passing: {obj} ({e})" + ) + LOGGER.error(error_message) + raise type(e)(error_message) from e + else: + obj = Literal(obj) + return sub, pred, obj + + +def triple_to_string( + triple: Triple, + line_end: Optional[str] = None, +) -> str: + """Convert a triple to its string representation suitable for SPARQL queries. + + Args: + triple (Triple): The triple to convert. + Returns: + str: The string representation of the triple. + """ + sub, pred, obj = triple + return f"{sub.n3()} {pred.n3()} {obj.n3()}" + (f" {line_end}" if line_end else "") + def validate_query(query: str): + """ + Validate a SPARQL SELECT/ASK query string by parsing it. + + Args: + query (str): The SPARQL query to validate. + + Returns: + bool: True if parsing succeeds. + + Raises: + InvalidQueryError: If parsing fails. + """ try: # Attempt to prepare the query prepareQuery(query) @@ -24,7 +133,21 @@ def validate_query(query: str): raise InvalidQueryError(error_message) -def validate_update_query(query: str): +def validate_update_query( + query: str, +): + """ + Validate a SPARQL UPDATE string by applying it to a temporary dataset. + + Args: + query (str): The SPARQL UPDATE string to validate. + + Returns: + bool: True if validation succeeds. + + Raises: + InvalidQueryError: If validation fails. + """ try: g = Dataset() g.update(query) @@ -35,54 +158,22 @@ def validate_update_query(query: str): raise InvalidQueryError(error_message) -def ensure_absolute(iri: str): - """Ensure the IRI is in absolute form enclosed in <>. - - If the IRI is already absolute (i.e., enclosed in <>), it returns as is. - Otherwise, it wraps the IRI in <>. - - Args: - iri (str): The input IRI. - - Returns: - str: The absolute IRI in <> format. +def to_literal( + value: Any, + datatype: Optional[str] = None, + as_string: Optional[bool] = False, +) -> Union[Literal, str]: """ - iri = iri.strip() - - # Check if already enclosed in <> - if iri.startswith("<") and iri.endswith(">"): - return iri - - return f"<{iri}>" - - -def is_absolute(iri: str) -> bool: - """Check if the IRI is in absolute form. + Convert a Python value to an XSD literal. Args: - iri (str): The input IRI. + value (Any): The Python value to convert. + datatype: (Optional[str]) Optional XSD datatype to use; inferred as `XSD.string` for strings. + as_string (Optional[bool]): If True, return the N3 string form of the literal. Defaults to False. Returns: - bool: True if the IRI is absolute, False otherwise. + Union[Literal, str]: The `rdflib.Literal` or its N3 string form when `as_string=True`. """ - return iri.startswith("<") and iri.endswith(">") - - -def strip_angle_brackets(iri: str) -> str: - """Strip the angle brackets from the IRI if present. - - Args: - iri (str): The input IRI. - - Returns: - str: The IRI without angle brackets. - """ - # Remove angle brackets if they exist - return iri[1:-1] if is_absolute(iri) else iri - - -def to_literal(value, datatype=None, as_string: bool = False) -> Union[Literal, str]: - """Convert a Python value to its corresponding XSD literal representation.""" if isinstance(value, str) and datatype is None: datatype = XSD.string literal = Literal(value, datatype=datatype) @@ -92,220 +183,86 @@ def to_literal(value, datatype=None, as_string: bool = False) -> Union[Literal, return literal -def from_xsd_literal(value: str, datatype: str): - """ - Convert a string value to its corresponding Python type based on the XSD datatype. +def from_xsd_literal( + value: str, + datatype: str, +): """ - literal = Literal(value, datatype=datatype) - return literal.toPython() - - -def convert_query_result_to_python_type(result_binding: dict) -> Any: - """Convert a SPARQL query result binding to its corresponding Python type.""" - type = result_binding.get("type") - if type == "literal" and "datatype" in result_binding: - return from_xsd_literal(result_binding["value"], result_binding["datatype"]) - else: - # If no datatype is provided, return the value as is - return result_binding["value"] - - -def get_local_name(iri: str): - iri = URIRef(strip_angle_brackets(iri)) - # If there's a fragment (i.e., the part after '#') - if iri.fragment: - return iri.fragment - - # Otherwise, split by '/' and return the last segment - return iri.split("/")[-1] - - -def escape_string_literal(value: Union[str, Literal]) -> Union[Literal, str]: - if ( - isinstance(value, Literal) - and isinstance(value.value, str) - # Try to prevent double escaping. - and not '\\"' in value - ): - value = value.replace('"', '\\"') - return Literal(f'"{value}"', datatype=XSD.string) - - return value - - -def is_iri(value: str) -> bool: - """Checks if the provided value is a valid IRI.""" - stripped = strip_angle_brackets(value) - parseresult = urlparse(stripped) - if not parseresult.scheme or not parseresult.netloc: - return False - return True - - -def is_shorthand_iri(value: str, prefixes: Optional[Dict[str, str]] = None) -> bool: - """ - Checks if the provided value is in the form of a shorthand IRI (prefix:localName). - - A shorthand IRI consists of a prefix and a local name separated by a colon (":"). - This function verifies if the given value matches this format and if a dict of prefixes - is given in the provided dictionary of prefixes. - - value (str): The string to check if it is a shorthand IRI. - prefixes (Optional[Dict[str, str]]): A dictionary mapping prefixes to their full IRIs. - - bool: True if the value is in the form of a valid shorthand IRI, False otherwise. - """ - if is_iri(value): - return False - elif ":" in value: - # Check if value can be splitted exactly in two parts - if len(value.split(":")) != 2: - return False - prefix = value.split(":")[0] - if prefixes: - # Check if the prefix exists in the provided prefixes dictionary - if prefix in prefixes: - return True - else: - LOGGER.warning( - f"Prefix '{prefix}' not found in the provided prefixes dictionary." - ) - return False - else: - # If no prefixes are provided, just check the format - return True - else: - return False - - -def prepare_subject(sub: str, ensure_iri: bool = True) -> str: - """ - Prepares and validates a subject string, ensuring it conforms to IRI (Internationalized Resource Identifier) - standards if required. + Convert an XSD-typed string value to a Python value. Args: - sub (str): The subject string to validate and prepare. - ensure_iri (bool, optional): If True, ensures the subject is a valid IRI. Defaults to True. + value (str): The lexical form of the literal. + datatype (str): The XSD datatype IRI. Returns: - str: The prepared subject string, either as an absolute IRI or as provided if valid. - - Raises: - InvalidInputError: If the provided subject is not a string. - InvalidIRIError: If the subject is not a valid IRI and `ensure_iri` is True. + Any: The converted Python value. """ - if not type(sub) == str: - raise InvalidInputError(f"Provided subject '{sub}' is not a string.") - if is_iri(sub): - return ensure_absolute(sub) - elif is_shorthand_iri(sub): - return sub - else: - if ensure_iri is True: - raise InvalidIRIError( - f"Provided subject '{sub}' is not a valid IRI. Ensure 'ensure_iri' is set correctly." - ) - else: - return sub + literal = Literal(value, datatype=datatype) + return literal.toPython() -def prepare_predicate(pred: str, ensure_iri: bool = True) -> str: +def convert_query_result_to_python_type( + result_binding: dict, +) -> Any: """ - Prepares a predicate string by validating and optionally ensuring it is an IRI (Internationalized Resource Identifier). + Convert a SPARQL binding entry to a Python value. Args: - pred (str): The predicate to be validated and processed. - ensure_iri (bool, optional): If True, ensures the predicate is a valid IRI. Defaults to True. + result_binding (dict): A single binding dict (e.g., `{ 'type': 'literal', ... }`). Returns: - str: The processed predicate, either as an absolute IRI or as provided if valid. - - Raises: - InvalidInputError: If the provided predicate is not a string. - InvalidIRIError: If `ensure_iri` is True and the provided predicate is not a valid IRI. + Any: A Python value converted from the binding, or the raw string when not typed. """ - if not type(pred) == str: - raise InvalidInputError(f"Provided subject '{pred}' is not a string.") - if is_iri(pred): - return ensure_absolute(pred) - elif is_shorthand_iri(pred): - return pred + type = result_binding.get("type") + if type == "literal" and "datatype" in result_binding: + return from_xsd_literal(result_binding["value"], result_binding["datatype"]) + elif type == "uri": + return IRI(result_binding["value"]) else: - if ensure_iri is True: - raise InvalidIRIError( - f"Provided predicate '{pred}' is not a valid IRI. Ensure 'ensure_iri' is set correctly." - ) - else: - return pred + # If no datatype is provided, return the value as is + return result_binding["value"] -def prepare_object( - obj: Any, as_string: bool = False, ensure_iri: bool = False -) -> Union[str, Literal]: +def get_local_name( + iri: str, +) -> str: """ - Prepares an object for use in a graph database context by ensuring it is in the - correct format, such as an IRI (Internationalized Resource Identifier) or a Literal. + Extract the local name from an IRI. + + Prefers the fragment after `#` when present; otherwise returns the last path + segment after `/`. Args: - obj (Any): The object to be prepared. It can be a string, Literal, or any other type. - as_string (bool, optional): If True, converts a Literal object to its string representation. - Defaults to False. - ensure_iri (bool, optional): If True, ensures that the provided object is a valid IRI. - Raises an InvalidIRIError if the object is not a valid IRI. Defaults to False. + iri (str): The input IRI (full or shorthand acceptable). Returns: - Union[str, Literal]: The prepared object. This can be: - - A string representing an absolute or shorthand IRI. - - A Literal object or its string representation if `as_string` is True. - - Raises: - InvalidIRIError: If `ensure_iri` is True and the provided object is not a valid IRI. + str: The local name component. """ - if ensure_iri: - if not type(obj) == str: - raise InvalidIRIError( - f"Provided object '{obj}' is not a string. Cannot be a valid IRI." - ) - if is_iri(obj): - return ensure_absolute(obj) - elif is_shorthand_iri(obj): - return obj - else: - raise InvalidIRIError( - f"Provided object '{obj}' is not a valid IRI. Ensure 'ensure_iri' is set correctly." - ) - - if type(obj) == str: - if is_iri(obj): - return ensure_absolute(obj) - else: - return obj - - if type(obj) == Literal: - # TODO: How to handle string escapes, obj: Literal = escape_string_literal(obj) - if as_string: - return obj.n3() - else: - return obj + iri = IRI(iri) + # If there's a fragment (i.e., the part after '#') + if iri.fragment: + return iri.fragment - return to_literal(obj, as_string=as_string) + # Otherwise, split by '/' and return the last segment + return iri.split("/")[-1] -def encapsulate_named_graph(named_graph: Optional[str], content: str) -> str: +def encapsulate_named_graph( + named_graph: IRI, + content: str, +) -> str: """ Encapsulates the given content within a named graph block if a named graph is provided. Args: - named_graph (Optional[str]): The IRI of the named graph. If None, the content is returned as is. + named_graph (IRI): The IRI of the named graph. If None, the content is returned as is. content (str): The SPARQL content to encapsulate. Returns: str: The encapsulated content or the original content if no named graph is provided. """ if named_graph: - named_graph = ensure_absolute(named_graph) - return f""" -GRAPH {named_graph} {{ + return f"""GRAPH {named_graph.n3()} {{ {content} }}""" return content From 25a6d8ea1a3d4813c148916a4d2ea099df1c05e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 1 Dec 2025 20:27:21 +0100 Subject: [PATCH 06/36] Update requirements: Add pydantic --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 954af13..b019600 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ python = "^3.11" rdflib = "==7.1.3" requests = "^2.32.3" python-snappy = "^0.7.3" +pydantic = "^2.11.7" [tool.poetry.group.dev.dependencies] black = "^25.1.0" From ef2b3ab35d164b9419bdfb870629b6aef83150d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Tue, 2 Dec 2025 10:26:03 +0100 Subject: [PATCH 07/36] Fix nested quotes bug --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b019600..aeed5b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = ["Jan-Felix Klein ", "Nico Brandt Date: Tue, 2 Dec 2025 16:09:20 +0100 Subject: [PATCH 08/36] Fix local named graph overrides - Add named_graph argument to all triple interacting methods - Add support for str and IRI named class arguments - Add test fixtures to both test local and global named graph assignments - Fix named_graph argument propagation --- .../queries/ontology_helpers.py | 29 +- graph_db_interface/queries/triple_multi.py | 74 +++-- graph_db_interface/queries/triple_single.py | 62 ++-- graph_db_interface/sparql_query.py | 6 +- tests/conftest.py | 1 + tests/test_graph_manipulation.py | 314 ++++++++++++++---- tests/test_triple_get.py | 109 ++++-- 7 files changed, 430 insertions(+), 165 deletions(-) diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index 5e5969e..76d2be4 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -20,6 +20,7 @@ def iri_exists( as_obj: Optional[bool] = False, include_explicit: Optional[bool] = True, include_implicit: Optional[bool] = True, + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Check if an IRI exists as subject, predicate, or object. @@ -31,6 +32,7 @@ def iri_exists( as_obj (Optional[bool]): If True, check existence as object. Defaults to False. include_explicit (Optional[bool]): Include explicit triples (`FROM onto:explicit`). Defaults to True. include_implicit (Optional[bool]): Include inferred triples (`FROM onto:implicit`). Defaults to True. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if the IRI exists based on the specified criteria, False otherwise. @@ -46,6 +48,7 @@ def iri_exists( ) iri = IRI(iri) + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph # Define potential query parts where_clauses = [] @@ -56,7 +59,7 @@ def iri_exists( if as_obj: where_clauses.append(f"{{?s ?p {iri.n3()} . }}") query = SPARQLQuery( - named_graph=self.named_graph, + named_graph=named_graph, include_explicit=include_explicit, include_implicit=include_implicit, ) @@ -83,6 +86,7 @@ def is_subclass( self: "GraphDB", subclass_iri: Union[str, IRI], class_iri: Union[str, IRI], + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Check whether one class is a subclass of another (`rdfs:subClassOf`). @@ -92,14 +96,22 @@ def is_subclass( Args: subclass_iri (Union[str, IRI]): The IRI of the potential subclass. class_iri (Union[str, IRI]): The IRI of the potential superclass. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if `subclass_iri` is a subclass of `class_iri`, False otherwise. """ - return self.triple_exists((subclass_iri, "rdfs:subClassOf", class_iri)) + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph + return self.triple_exists( + (subclass_iri, "rdfs:subClassOf", class_iri), named_graph=named_graph + ) -def owl_is_named_individual(self: "GraphDB", iri: Union[str, IRI]) -> bool: +def owl_is_named_individual( + self: "GraphDB", + iri: Union[str, IRI], + named_graph: Optional[Union[str, IRI]] = None, +) -> bool: """ Check if the given IRI corresponds to an OWL named individual. @@ -107,11 +119,15 @@ def owl_is_named_individual(self: "GraphDB", iri: Union[str, IRI]) -> bool: Args: iri (Union[str, IRI]): The IRI to check. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if the IRI is a named individual, False otherwise. """ - return self.triple_exists((iri, "rdf:type", "owl:NamedIndividual")) + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph + return self.triple_exists( + (iri, "rdf:type", "owl:NamedIndividual"), named_graph=named_graph + ) def owl_get_classes_of_individual( @@ -121,6 +137,7 @@ def owl_get_classes_of_individual( local_name: Optional[bool] = False, include_explicit: Optional[bool] = True, include_implicit: Optional[bool] = False, + named_graph: Optional[Union[str, IRI]] = None, ) -> List[Union[IRI, str]]: """ Get the OWL classes associated with a given individual. @@ -135,11 +152,13 @@ def owl_get_classes_of_individual( local_name (Optional[bool]): If True, return only the local names of the classes. Defaults to False. include_explicit (Optional[bool]): Include explicit triples. Defaults to True. include_implicit (Optional[bool]): Include inferred triples. Defaults to False. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: List[Union[IRI, str]]: Class IRIs, or local names if `local_name=True`. """ instance_iri = IRI(instance_iri) + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph ignored_prefixes = ( ignored_prefixes if ignored_prefixes is not None else ["owl", "rdfs"] ) @@ -159,7 +178,7 @@ def owl_get_classes_of_individual( filter_conditions = "" query = SPARQLQuery( - named_graph=self.named_graph, + named_graph=named_graph, include_explicit=include_explicit, include_implicit=include_implicit, ) diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index 9aadc4e..f0b9eeb 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -21,6 +21,7 @@ def triples_get( obj: Optional[Union[str, IRI, Literal]] = None, include_explicit: Optional[bool] = True, include_implicit: Optional[bool] = True, + named_graph: Optional[Union[str, IRI]] = None, ) -> List[Tuple[IRI, IRI, Any]]: """ Retrieve triples matching any combination of subject, predicate, or object. @@ -33,6 +34,7 @@ def triples_get( obj (Optional[Union[str, IRI, Literal]]): Object filter (IRI/shorthand/Literal/string). include_explicit (Optional[bool]): Include explicit triples. Defaults to True. include_implicit (Optional[bool]): Include inferred triples. Defaults to True. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: List[Tuple[IRI, IRI, Any]]: Matching triples as `(subject, predicate, object)`, where the @@ -48,6 +50,8 @@ def triples_get( "Either 'triple' or 'sub/pred/obj' must be provided, not both." ) + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph + sub, pred, obj = utils.sanitize_triple( triple or (sub, pred, obj), allow_partial=True ) @@ -78,7 +82,7 @@ def _append_bind_and_filter( _append_bind_and_filter("?o", obj) query = SPARQLQuery( - named_graph=self.named_graph, + named_graph=named_graph, include_explicit=include_explicit, include_implicit=include_implicit, ) @@ -108,18 +112,19 @@ def _append_bind_and_filter( def any_triple_exists( self: "GraphDB", triples: List[Triple], - named_graph: Optional[IRI] = None, + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Check if any of the given triples exist. Args: triples (List[Triple]): Triples to check. - named_graph (Optional[IRI]): Override the client's default named graph. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if at least one exists, False otherwise. """ + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph if not triples: raise InvalidInputError(f"Cannot check existence of empty triple list.") @@ -127,14 +132,14 @@ def any_triple_exists( for triple in triples: triple = utils.sanitize_triple(triple) - if self.triple_exists(triple): + if self.triple_exists(triple, named_graph=named_graph): self.logger.debug( - f"At least this triple exists: ({utils.triple_to_string(triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"At least this triple exists: ({utils.triple_to_string(triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return True self.logger.debug( - f"None of the triples exists: ({triples}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"None of the triples exists: ({triples}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return False @@ -142,18 +147,19 @@ def any_triple_exists( def all_triple_exists( self: "GraphDB", triples: List[Triple], - named_graph: Optional[IRI] = None, + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Check if all of the given triples exist. Args: triples (List[Triple]): Triples to check. - named_graph (Optional[IRI]): Override the client's default named graph. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if all exist, False otherwise. """ + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph if not triples: raise InvalidInputError(f"Cannot check existence of empty triple list.") @@ -163,28 +169,28 @@ def all_triple_exists( triple = utils.sanitize_triple(triple) triple_strings.append(utils.triple_to_string(triple, ".")) - query = SPARQLQuery(named_graph=named_graph or self.named_graph) + query = SPARQLQuery(named_graph=named_graph) query.add_ask_block(where_clauses=triple_strings) query_string = query.to_string() if not query_string: raise InvalidInputError( - f"Could not generate 'all_triple_exists' query for triples ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Could not generate 'all_triple_exists' query for triples ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) ask_result = self.query(query=query_string, update=False) if ask_result is None: raise InvalidInputError( - f"Could not query 'all_triple_exists' for triples ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Could not query 'all_triple_exists' for triples ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) if ask_result["boolean"] is False: self.logger.debug( - f"Not all of the triples exist: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Not all of the triples exist: ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return False self.logger.debug( - f"All of the triples exist: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"All of the triples exist: ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return True @@ -193,7 +199,7 @@ def triples_add( self: "GraphDB", triples_to_add: List[Triple], check_exist: Optional[bool] = True, - named_graph: Optional[IRI] = None, + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Add multiple triples to the graph database. @@ -201,11 +207,13 @@ def triples_add( Args: triples_to_add (List[Triple]): Triples to add. check_exist (Optional[bool]): If True, abort when any triple already exists. Defaults to True. - named_graph (Optional[IRI]): Override the client's default named graph. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if all triples were added, False otherwise. """ + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph + if not triples_to_add: return True @@ -225,25 +233,25 @@ def triples_add( utils.triple_to_string(triple, ".") for triple in validated_triples_to_add ] - query = SPARQLQuery(named_graph=named_graph or self.named_graph) + query = SPARQLQuery(named_graph=named_graph) query.add_insert_data_block( triples=validated_triples_to_add, ) query_string = query.to_string() if not query_string: raise InvalidInputError( - f"Could not generate 'triples_add' query for triples ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Could not generate 'triples_add' query for triples ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) result = self.query(query=query_string, update=True) if not result: self.logger.warning( - f"Failed to add triples: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Failed to add triples: ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return False self.logger.debug( - f"Successfully added triples: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Successfully added triples: ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return result @@ -252,7 +260,7 @@ def triples_delete( self: "GraphDB", triples_to_delete: List[Triple], check_exist: Optional[bool] = True, - named_graph: Optional[IRI] = None, + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Delete multiple triples from the graph database. @@ -260,11 +268,13 @@ def triples_delete( Args: triples_to_delete (List[Triple]): Triples to delete. check_exist (Optional[bool]): If True, abort when any triple does not exist. Defaults to True. - named_graph (Optional[IRI]): Override the client's default named graph. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if all triples were deleted, False otherwise. """ + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph + if not triples_to_delete: return True @@ -284,25 +294,25 @@ def triples_delete( utils.triple_to_string(triple, ".") for triple in validated_triples_to_delete ] - query = SPARQLQuery(named_graph=named_graph or self.named_graph) + query = SPARQLQuery(named_graph=named_graph) query.add_delete_data_block( triples=validated_triples_to_delete, ) query_string = query.to_string() if not query_string: raise InvalidInputError( - f"Could not generate 'triples_delete' query for triples ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Could not generate 'triples_delete' query for triples ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) result = self.query(query=query_string, update=True) if not result: self.logger.warning( - f"Failed to delete triples: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Failed to delete triples: ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return False self.logger.debug( - f"Successfully deleted triples: ({triple_strings}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Successfully deleted triples: ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return True @@ -312,7 +322,7 @@ def triples_update( old_triples: List[Triple], new_triples: List[Triple], check_exist: Optional[bool] = True, - named_graph: Optional[IRI] = None, + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Update multiple RDF triples in the triplestore. @@ -321,11 +331,13 @@ def triples_update( old_triples (List[Triple]): Triples to be replaced. new_triples (List[Triple]): Replacement triples (same length as `old_triples`). check_exist (Optional[bool]): If True, abort when any old triple does not exist. Defaults to True. - named_graph (Optional[IRI]): Override the client's default named graph. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if the update was successful, False otherwise. """ + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph + if not old_triples and not new_triples: return True @@ -347,7 +359,7 @@ def triples_update( utils.triple_to_string(triple, ".") for triple in validated_old_triples ] - query = SPARQLQuery(named_graph=named_graph or self.named_graph) + query = SPARQLQuery(named_graph=named_graph) query.add_delete_insert_data_block( delete_triples=validated_old_triples, insert_triples=validated_new_triples, @@ -356,17 +368,17 @@ def triples_update( query_string = query.to_string(validate=True) if not query_string: raise InvalidInputError( - f"Could not generate 'triples_update' query for triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Could not generate 'triples_update' query for triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) result = self.query(query=query_string, update=True) if not result: self.logger.warning( - f"Failed to update triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Failed to update triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return False self.logger.debug( - f"Successfully updated triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Successfully updated triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return True diff --git a/graph_db_interface/queries/triple_single.py b/graph_db_interface/queries/triple_single.py index 0f5d16f..5231765 100644 --- a/graph_db_interface/queries/triple_single.py +++ b/graph_db_interface/queries/triple_single.py @@ -1,6 +1,6 @@ # To be imported into ..graph_db.py GraphDB class -from typing import Optional, TYPE_CHECKING +from typing import Optional, Union, TYPE_CHECKING from graph_db_interface.utils import utils from graph_db_interface.utils.iri import IRI from graph_db_interface.utils.utils import ( @@ -18,20 +18,20 @@ def triple_exists( self: "GraphDB", triple: Triple, - named_graph: Optional[IRI] = None, + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Check whether a specific triple exists in the graph database. Args: triple (Triple): The triple `(subject, predicate, object)` to check. - named_graph (Optional[IRI]): Override the client's default named graph. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if the triple exists, False otherwise. """ triple = utils.sanitize_triple(triple) - named_graph = named_graph or self.named_graph + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph query = SPARQLQuery(named_graph=named_graph) query.add_ask_block( @@ -44,12 +44,12 @@ def triple_exists( result = self.query(query=query_string) if result is None or result["boolean"] is False: self.logger.debug( - f"Unable to find triple ({utils.triple_to_string(triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Unable to find triple ({utils.triple_to_string(triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return False self.logger.debug( - f"Found triple ({utils.triple_to_string(triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Found triple ({utils.triple_to_string(triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return True @@ -57,21 +57,22 @@ def triple_exists( def triple_add( self: "GraphDB", triple: Triple, - named_graph: Optional[IRI] = None, + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Add a triple to the graph database. Args: triple (Triple): The triple `(subject, predicate, object)` to insert. - named_graph (Optional[IRI]): Override the client's default named graph. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if the triple was inserted, False otherwise. """ triple = utils.sanitize_triple(triple) + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph - query = SPARQLQuery(named_graph=named_graph or self.named_graph) + query = SPARQLQuery(named_graph=named_graph) query.add_insert_data_block( triples=[triple], ) @@ -82,12 +83,12 @@ def triple_add( result = self.query(query=query_string, update=True) if not result: self.logger.warning( - f"Failed to insert triple: ({utils.triple_to_string(triple)}) named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Failed to insert triple: ({utils.triple_to_string(triple)}) named_graph: {named_graph or "default"}, repository: {self._repository}" ) return False self.logger.debug( - f"Successfully inserted triple: ({utils.triple_to_string(triple)}) named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Successfully inserted triple: ({utils.triple_to_string(triple)}) named_graph: {named_graph or "default"}, repository: {self._repository}" ) return True @@ -97,7 +98,7 @@ def triple_delete( self: "GraphDB", triple: Triple, check_exist: Optional[bool] = True, - named_graph: Optional[IRI] = None, + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Delete a single triple. @@ -109,18 +110,19 @@ def triple_delete( Args: triple (Triple): The triple `(subject, predicate, object)` to delete. check_exist (Optional[bool]): Whether to verify existence prior to deletion. Defaults to True. - named_graph (Optional[IRI]): Override the client's default named graph. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if deletion succeeded (or triple absent when `check_exist=False`), False otherwise. """ triple = utils.sanitize_triple(triple) + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph if check_exist: - if not self.triple_exists(triple): + if not self.triple_exists(triple, named_graph=named_graph): self.logger.warning("Unable to delete triple since it does not exist") return False - query = SPARQLQuery(named_graph=named_graph or self.named_graph) + query = SPARQLQuery(named_graph=named_graph) query.add_delete_data_block( triples=[triple], ) @@ -132,12 +134,12 @@ def triple_delete( result = self.query(query=query_string, update=True) if not result: self.logger.warning( - f"Failed to delete triple: ({utils.triple_to_string(triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Failed to delete triple: ({utils.triple_to_string(triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return False self.logger.debug( - f"Successfully deleted triple: ({utils.triple_to_string(triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Successfully deleted triple: ({utils.triple_to_string(triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return True @@ -146,11 +148,11 @@ def triple_update( self: "GraphDB", old_triple: Triple, new_triple: Optional[PartialTriple] = None, - new_sub: Optional[IRI] = None, - new_pred: Optional[IRI] = None, - new_obj: Optional[IRI] = None, + new_sub: Optional[Union[str, IRI]] = None, + new_pred: Optional[Union[str, IRI]] = None, + new_obj: Optional[Union[str, IRI]] = None, check_exist: Optional[bool] = True, - named_graph: Optional[IRI] = None, + named_graph: Optional[Union[str, IRI]] = None, ) -> bool: """ Update a triple by replacing any of its parts. @@ -162,11 +164,11 @@ def triple_update( old_triple (Triple): Existing triple to update. new_triple (Optional[PartialTriple]): Replacement values (subject/predicate/object). Use this or `new_sub`/`new_pred`/`new_obj`. - new_sub (Optional[IRI]): Replacement subject. - new_pred (Optional[IRI]): Replacement predicate. - new_obj (Optional[IRI]): Replacement object. + new_sub (Optional[Union[str, IRI]]): Replacement subject. + new_pred (Optional[Union[str, IRI]]): Replacement predicate. + new_obj (Optional[Union[str, IRI]]): Replacement object. check_exist (Optional[bool]): If True, verify that `old_triple` exists before updating. Defaults to True. - named_graph (Optional[IRI]): Override the client's default named graph. + named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. Returns: bool: True if the update succeeded, False otherwise. @@ -187,8 +189,10 @@ def triple_update( allow_partial=True, ) + named_graph = IRI(named_graph) if named_graph is not None else self.named_graph + if check_exist: - if not self.triple_exists(old_triple): + if not self.triple_exists(old_triple, named_graph=named_graph): self.logger.warning( f"Triple does not exist: ({utils.triple_to_string(old_triple)})" ) @@ -197,7 +201,7 @@ def triple_update( # Determine replacement variables update_triple = tuple(n if n else o for o, n in zip(old_triple, new_triple)) - query = SPARQLQuery(named_graph=named_graph or self.named_graph) + query = SPARQLQuery(named_graph=named_graph) query.add_delete_insert_data_block( delete_triples=[old_triple], insert_triples=[update_triple], @@ -210,11 +214,11 @@ def triple_update( result = self.query(query=query_string, update=True) if not result: self.logger.warning( - f"Failed to update triple to: ({utils.triple_to_string(update_triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Failed to update triple to: ({utils.triple_to_string(update_triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return False self.logger.debug( - f"Successfully updated triple to: ({utils.triple_to_string(update_triple)}), named_graph: {named_graph or self.named_graph or "default"}, repository: {self._repository}" + f"Successfully updated triple to: ({utils.triple_to_string(update_triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" ) return True diff --git a/graph_db_interface/sparql_query.py b/graph_db_interface/sparql_query.py index 9cba61c..584c3a4 100644 --- a/graph_db_interface/sparql_query.py +++ b/graph_db_interface/sparql_query.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import List, Optional +from typing import List, Optional, Union from graph_db_interface.utils import utils from graph_db_interface.utils.utils import Triple from graph_db_interface.utils.iri import IRI @@ -30,11 +30,11 @@ class SPARQLQuery: def __init__( self, - named_graph: Optional[IRI] = None, + named_graph: Optional[Union[str, IRI]] = None, include_explicit: Optional[bool] = True, include_implicit: Optional[bool] = True, ): - self._named_graph = named_graph + self._named_graph = IRI(named_graph) if named_graph is not None else None self._include_explicit = include_explicit self._include_implicit = include_implicit self._query_blocks = [] diff --git a/tests/conftest.py b/tests/conftest.py index 4887b62..c72cd6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,6 +31,7 @@ def db() -> GraphDB: for graph in [ None, "http://example.org/named_graph", + "http://example.org/local_named_graph", ]: db.clear_graph(graph) diff --git a/tests/test_graph_manipulation.py b/tests/test_graph_manipulation.py index ba7052e..566f610 100644 --- a/tests/test_graph_manipulation.py +++ b/tests/test_graph_manipulation.py @@ -4,7 +4,8 @@ from graph_db_interface.exceptions import InvalidInputError from graph_db_interface.utils import utils -NAMED_GRAPH = "http://example.org/named_graph" +GLOBAL_NAMED_GRAPH = "http://example.org/named_graph" +LOCAL_NAMED_GRAPH = "http://example.org/local_named_graph" SUB_1 = "http://example.org#subject_1" PRED_1 = "http://example.org#predicate_1" @@ -23,38 +24,57 @@ NEW_OBJ_2 = True -@pytest.fixture(params=[None, NAMED_GRAPH], scope="module", autouse=True) +@pytest.fixture(params=[None, LOCAL_NAMED_GRAPH], scope="module") +def named_graph(request) -> str: + # Provide a per-test local override for the named graph + return request.param + + +@pytest.fixture(params=[None, GLOBAL_NAMED_GRAPH], scope="module", autouse=True) def setup(request, db: GraphDB): - named_graph = request.param - # We once set a named graph and once we don't - db.named_graph = named_graph + # Set or unset the global named graph on the DB client + db.named_graph = request.param -def test_add_and_delete_triple(db: GraphDB): +def test_add_and_delete_triple(db: GraphDB, named_graph: str): # Add a new triple - result = db.triple_add((SUB_1, PRED_1, OBJ_1)) + result = db.triple_add( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True # try to delete the triple - result = db.triple_delete((SUB_1, PRED_1, OBJ_1)) + result = db.triple_delete( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True # try to delete the triple again - result = db.triple_delete((SUB_1, PRED_1, OBJ_1)) + result = db.triple_delete( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is False # if we dont check for existence it shoud return True - result = db.triple_delete((SUB_1, PRED_1, OBJ_1), check_exist=False) + result = db.triple_delete( + (SUB_1, PRED_1, OBJ_1), + check_exist=False, + named_graph=named_graph, + ) assert result is True -def test_add_and_delete_multiple_triples(db: GraphDB): +def test_add_and_delete_multiple_triples(db: GraphDB, named_graph: str): # add multiple triples result = db.triples_add( [ (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is True @@ -63,7 +83,8 @@ def test_add_and_delete_multiple_triples(db: GraphDB): [ (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is True @@ -72,7 +93,8 @@ def test_add_and_delete_multiple_triples(db: GraphDB): [ (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is False @@ -83,19 +105,26 @@ def test_add_and_delete_multiple_triples(db: GraphDB): (SUB_2, PRED_2, OBJ_2), ], check_exist=False, + named_graph=named_graph, ) assert result is True -def test_update_triple(db: GraphDB): +def test_update_triple(db: GraphDB, named_graph: str): # Add a new triple - result = db.triple_add((SUB_1, PRED_1, OBJ_1)) + result = db.triple_add( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True # Input errors # no new given with pytest.raises(InvalidInputError): - db.triple_update(old_triple=(SUB_1, PRED_1, OBJ_1)) + db.triple_update( + old_triple=(SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) # both new given with pytest.raises(InvalidInputError): @@ -103,6 +132,7 @@ def test_update_triple(db: GraphDB): old_triple=(SUB_1, PRED_1, OBJ_1), new_triple=(NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), new_sub=NEW_SUB_1, + named_graph=named_graph, ) # old is incomplete @@ -110,28 +140,37 @@ def test_update_triple(db: GraphDB): db.triple_update( old_triple=(SUB_1, PRED_1, None), new_triple=(NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), + named_graph=named_graph, ) # try to update the full triple result = db.triple_update( old_triple=(SUB_1, PRED_1, OBJ_1), new_triple=(NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), + named_graph=named_graph, ) assert result is True # try to update the individual entries result = db.triple_update( - old_triple=(NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), new_sub=SUB_1 + old_triple=(NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), + new_sub=SUB_1, + named_graph=named_graph, ) assert result is True result = db.triple_update( old_triple=(SUB_1, NEW_PRED_1, NEW_OBJ_1), new_pred=PRED_1, + named_graph=named_graph, ) assert result is True - result = db.triple_update(old_triple=(SUB_1, PRED_1, NEW_OBJ_1), new_obj=OBJ_1) + result = db.triple_update( + old_triple=(SUB_1, PRED_1, NEW_OBJ_1), + new_obj=OBJ_1, + named_graph=named_graph, + ) assert result is True # adressing via individual arguments @@ -140,72 +179,98 @@ def test_update_triple(db: GraphDB): new_sub=NEW_SUB_1, new_pred=NEW_PRED_1, new_obj=NEW_OBJ_1, + named_graph=named_graph, ) assert result is True # Cleanup - result = db.triple_delete((NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1)) + result = db.triple_delete( + (NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), + named_graph=named_graph, + ) assert result is True -def test_update_triple_only_subject(db: GraphDB): +def test_update_triple_only_subject(db: GraphDB, named_graph: str): # Add a new triple - result = db.triple_add((SUB_1, PRED_1, OBJ_1)) + result = db.triple_add( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True # only update the subject of the triple result = db.triple_update( old_triple=(SUB_1, PRED_1, OBJ_1), new_triple=(NEW_SUB_1, None, None), + named_graph=named_graph, ) assert result is True # try to delete the triple - result = db.triple_delete((NEW_SUB_1, PRED_1, OBJ_1)) + result = db.triple_delete( + (NEW_SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True -def test_update_triple_only_predicate(db: GraphDB): +def test_update_triple_only_predicate(db: GraphDB, named_graph: str): # Add a new triple - result = db.triple_add((SUB_1, PRED_1, OBJ_1)) + result = db.triple_add( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True # only update the predicate of the triple result = db.triple_update( old_triple=(SUB_1, PRED_1, OBJ_1), new_triple=(None, NEW_PRED_1, None), + named_graph=named_graph, ) assert result is True # try to delete the triple - result = db.triple_delete((SUB_1, NEW_PRED_1, OBJ_1)) + result = db.triple_delete( + (SUB_1, NEW_PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True -def test_update_triple_only_object(db: GraphDB): +def test_update_triple_only_object(db: GraphDB, named_graph: str): # Add a new triple - result = db.triple_add((SUB_1, PRED_1, OBJ_1)) + result = db.triple_add( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True # only update the object of the triple result = db.triple_update( old_triple=(SUB_1, PRED_1, OBJ_1), new_triple=(None, None, NEW_OBJ_1), + named_graph=named_graph, ) assert result is True # try to delete the triple - result = db.triple_delete((SUB_1, PRED_1, NEW_OBJ_1)) + result = db.triple_delete( + (SUB_1, PRED_1, NEW_OBJ_1), + named_graph=named_graph, + ) assert result is True -def test_update_multiple_triples(db: GraphDB): +def test_update_multiple_triples(db: GraphDB, named_graph: str): # add multiple triples result = db.triples_add( [ (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is True @@ -219,6 +284,7 @@ def test_update_multiple_triples(db: GraphDB): (NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), (NEW_SUB_2, NEW_PRED_2, NEW_OBJ_2), ], + named_graph=named_graph, ) assert result is True @@ -227,19 +293,26 @@ def test_update_multiple_triples(db: GraphDB): [ (NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), (NEW_SUB_2, NEW_PRED_2, NEW_OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is True -def test_iri_exists(db: GraphDB): +def test_iri_exists(db: GraphDB, named_graph: str): # add a new triple to the default graph - result = db.triple_add((SUB_1, PRED_1, OBJ_1)) + result = db.triple_add( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True # does not specify any part of a triple to look for with pytest.raises(InvalidInputError): - db.iri_exists(iri=SUB_1) + db.iri_exists( + iri=SUB_1, + named_graph=named_graph, + ) # IRI should exist like this result = db.iri_exists( @@ -247,13 +320,23 @@ def test_iri_exists(db: GraphDB): as_sub=True, include_explicit=True, include_implicit=False, + named_graph=named_graph, ) assert result is True - result = db.iri_exists(SUB_1, as_sub=True, as_pred=True) + result = db.iri_exists( + SUB_1, + as_sub=True, + as_pred=True, + named_graph=named_graph, + ) assert result is False - result = db.iri_exists(PRED_1, as_pred=True) + result = db.iri_exists( + PRED_1, + as_pred=True, + named_graph=named_graph, + ) assert result is True result = db.iri_exists( @@ -261,6 +344,7 @@ def test_iri_exists(db: GraphDB): as_obj=True, include_explicit=True, include_implicit=False, + named_graph=named_graph, ) assert result is False @@ -269,48 +353,74 @@ def test_iri_exists(db: GraphDB): as_pred=True, include_explicit=True, include_implicit=False, + named_graph=named_graph, ) assert result is False - result = db.triple_delete((SUB_1, PRED_1, OBJ_1)) + result = db.triple_delete( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True -def test_triple_exists(db: GraphDB): +def test_triple_exists(db: GraphDB, named_graph: str): # Test return on empty DB - result = db.triple_exists((SUB_1, PRED_1, OBJ_1)) + result = db.triple_exists( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is False # Add triple - result = db.triple_add((SUB_1, PRED_1, OBJ_1)) + result = db.triple_add( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True # Test if triple is now found - result = db.triple_exists((SUB_1, PRED_1, OBJ_1)) + result = db.triple_exists( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True # Test if modified triple is not found - result = db.triple_exists((SUB_2, PRED_1, OBJ_1)) + result = db.triple_exists( + (SUB_2, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is False - result = db.triple_exists((SUB_1, PRED_2, OBJ_1)) + result = db.triple_exists( + (SUB_1, PRED_2, OBJ_1), + named_graph=named_graph, + ) assert result is False - result = db.triple_exists((SUB_1, PRED_1, OBJ_2)) + result = db.triple_exists( + (SUB_1, PRED_1, OBJ_2), + named_graph=named_graph, + ) assert result is False # Cleanup - result = db.triple_delete((SUB_1, PRED_1, OBJ_1)) + result = db.triple_delete( + (SUB_1, PRED_1, OBJ_1), + named_graph=named_graph, + ) assert result is True -def test_multi_triple_exists(db: GraphDB): +def test_multi_triple_exists(db: GraphDB, named_graph: str): # Test return on empty DB result = db.any_triple_exists( [ (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is False @@ -318,7 +428,8 @@ def test_multi_triple_exists(db: GraphDB): [ (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is False @@ -327,7 +438,8 @@ def test_multi_triple_exists(db: GraphDB): [ (SUB_1, PRED_1, OBJ_1), (NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), - ] + ], + named_graph=named_graph, ) assert result is True @@ -336,7 +448,8 @@ def test_multi_triple_exists(db: GraphDB): [ (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is True @@ -344,12 +457,16 @@ def test_multi_triple_exists(db: GraphDB): [ (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is False # Add second triple - result = db.triple_add((SUB_2, PRED_2, OBJ_2)) + result = db.triple_add( + (SUB_2, PRED_2, OBJ_2), + named_graph=named_graph, + ) assert result is True # One triple now matches @@ -357,7 +474,8 @@ def test_multi_triple_exists(db: GraphDB): [ (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is True @@ -365,7 +483,8 @@ def test_multi_triple_exists(db: GraphDB): [ (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) assert result is True @@ -375,57 +494,108 @@ def test_multi_triple_exists(db: GraphDB): (SUB_1, PRED_1, OBJ_1), (SUB_2, PRED_2, OBJ_2), (NEW_SUB_1, NEW_PRED_1, NEW_OBJ_1), - ] + ], + named_graph=named_graph, ) assert result is True -def test_convenience_functions(db: GraphDB): +def test_convenience_functions(db: GraphDB, named_graph: str): sub = "http://example.org#instance" pred = "rdfs:subClassOf" obj = "http://example.org#myClass" - result = db.triple_add((sub, pred, obj)) + result = db.triple_add( + (sub, pred, obj), + named_graph=named_graph, + ) assert result is True - result = db.triple_add((sub, "rdf:type", "owl:NamedIndividual")) + result = db.triple_add( + (sub, "rdf:type", "owl:NamedIndividual"), + named_graph=named_graph, + ) assert result is True - result = db.triple_add((sub, "rdf:type", obj)) + result = db.triple_add( + (sub, "rdf:type", obj), + named_graph=named_graph, + ) assert result is True - result = db.triple_add((obj, "rdf:type", "owl:Class")) + result = db.triple_add( + (obj, "rdf:type", "owl:Class"), + named_graph=named_graph, + ) assert result is True - result = db.is_subclass(sub, obj) + result = db.is_subclass( + sub, + obj, + named_graph=named_graph, + ) assert result is True - result = db.is_subclass(sub, "http://example.org#someNonExistingClass") + result = db.is_subclass( + sub, + "http://example.org#someNonExistingClass", + named_graph=named_graph, + ) assert result is False - result = db.owl_is_named_individual(sub) + result = db.owl_is_named_individual( + sub, + named_graph=named_graph, + ) assert result is True - result = db.owl_is_named_individual(pred) + result = db.owl_is_named_individual( + pred, + named_graph=named_graph, + ) assert result is False - classes = db.owl_get_classes_of_individual(sub, local_name=False) + classes = db.owl_get_classes_of_individual( + sub, + local_name=False, + named_graph=named_graph, + ) assert classes == [obj] - classes = db.owl_get_classes_of_individual(sub, local_name=True) + classes = db.owl_get_classes_of_individual( + sub, + local_name=True, + named_graph=named_graph, + ) assert classes == [utils.get_local_name(obj)] - classes = db.owl_get_classes_of_individual(obj, local_name=False) + classes = db.owl_get_classes_of_individual( + obj, + local_name=False, + named_graph=named_graph, + ) assert classes == [] - result = db.triple_delete((sub, pred, obj)) + result = db.triple_delete( + (sub, pred, obj), + named_graph=named_graph, + ) assert result is True - result = db.triple_delete((sub, "rdf:type", "owl:NamedIndividual")) + result = db.triple_delete( + (sub, "rdf:type", "owl:NamedIndividual"), + named_graph=named_graph, + ) assert result is True - result = db.triple_delete((sub, "rdf:type", obj)) + result = db.triple_delete( + (sub, "rdf:type", obj), + named_graph=named_graph, + ) assert result is True - result = db.triple_delete((obj, "rdf:type", "owl:Class")) + result = db.triple_delete( + (obj, "rdf:type", "owl:Class"), + named_graph=named_graph, + ) assert result is True diff --git a/tests/test_triple_get.py b/tests/test_triple_get.py index c7065d5..cb01827 100644 --- a/tests/test_triple_get.py +++ b/tests/test_triple_get.py @@ -5,7 +5,8 @@ from rdflib import Literal, XSD import pytest -NAMED_GRAPH = "https://my_named_test_graph" +GLOBAL_NAMED_GRAPH = "https://my_named_test_graph" +LOCAL_NAMED_GRAPH = "http://example.org/local_named_graph" SUB_1 = "http://example.org#subject1" PRED_1 = "http://example.org#predicate1" @@ -16,98 +17,156 @@ OBJ_2 = "http://example.org#object2" -@pytest.fixture(params=[None, NAMED_GRAPH], scope="module", autouse=True) -def setup(request, db: GraphDB): +@pytest.fixture(params=[None, LOCAL_NAMED_GRAPH], scope="module") +def named_graph(request) -> str: + # Provide a per-test local override for the named graph + return request.param + + +@pytest.fixture(params=[None, GLOBAL_NAMED_GRAPH], scope="module", autouse=True) +def setup(request, db: GraphDB, named_graph: str): """Fixture to set up the test environment. Is called twice""" - named_graph = request.param + + global_named_graph = request.param # We once set a named graph and once we don't - db.named_graph = named_graph + db.named_graph = global_named_graph + + # prioritize local override over global override + named_graph = named_graph or global_named_graph db.triples_add( [ (SUB_1, PRED_1, OBJ_1), (SUBJ_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) yield db.triples_delete( [ (SUB_1, PRED_1, OBJ_1), (SUBJ_2, PRED_2, OBJ_2), - ] + ], + named_graph=named_graph, ) -def test_wrong_input(db: GraphDB): +def test_wrong_input(db: GraphDB, named_graph: str): # Neither sub, pred, obj given with pytest.raises(InvalidInputError): - db.triples_get() + db.triples_get( + named_graph=named_graph, + ) # Both triple and explicit iri given with pytest.raises(InvalidInputError): - db.triples_get((SUB_1, PRED_1, OBJ_1), sub=SUB_1) + db.triples_get( + (SUB_1, PRED_1, OBJ_1), + sub=SUB_1, + named_graph=named_graph, + ) -def test_triple_set_subjects(db: GraphDB): +def test_triple_set_subjects(db: GraphDB, named_graph: str): # Unenclosed absolute IRI - result_triples = db.triples_get(sub=SUB_1, include_implicit=False) - result_triples_wrong = db.triples_get(sub=PRED_1, include_implicit=False) + result_triples = db.triples_get( + sub=SUB_1, + include_implicit=False, + named_graph=named_graph, + ) + result_triples_wrong = db.triples_get( + sub=PRED_1, + include_implicit=False, + named_graph=named_graph, + ) assert result_triples == [(SUB_1, PRED_1, OBJ_1)] assert result_triples_wrong == [] # enclosed absolute IRI - result_triples = db.triples_get(sub=SUB_1, include_implicit=False) + result_triples = db.triples_get( + sub=SUB_1, + include_implicit=False, + named_graph=named_graph, + ) assert result_triples == [(SUB_1, PRED_1, OBJ_1)] # shorthand IRI IRI.add_prefix("ex", "http://example.org/") result_triples = db.triples_get( - sub=f"ex:{utils.get_local_name(SUB_1)}", include_implicit=False + sub=f"ex:{utils.get_local_name(SUB_1)}", + include_implicit=False, + named_graph=named_graph, ) assert result_triples == [(SUB_1, PRED_1, OBJ_1)] -def test_triple_set_predicates(db: GraphDB): +def test_triple_set_predicates(db: GraphDB, named_graph: str): # Unenclosed absolute IRI - result_triples = db.triples_get(pred=PRED_1, include_implicit=False) + result_triples = db.triples_get( + pred=PRED_1, + include_implicit=False, + named_graph=named_graph, + ) assert result_triples == [(SUB_1, PRED_1, OBJ_1)] # enclosed absolute IRI - result_triples = db.triples_get(pred=f"<{PRED_1}>", include_implicit=False) + result_triples = db.triples_get( + pred=f"<{PRED_1}>", + include_implicit=False, + named_graph=named_graph, + ) assert result_triples == [(SUB_1, PRED_1, OBJ_1)] # shorthand IRI IRI.add_prefix("ex", "http://example.org/") result_triples = db.triples_get( - pred=f"ex:{utils.get_local_name(PRED_1)}", include_implicit=False + pred=f"ex:{utils.get_local_name(PRED_1)}", + include_implicit=False, + named_graph=named_graph, ) assert result_triples == [(SUB_1, PRED_1, OBJ_1)] -def test_triple_set_objects(db: GraphDB): +def test_triple_set_objects(db: GraphDB, named_graph: str): # Unenclosed absolute IRI - result_triples = db.triples_get(obj=OBJ_2, include_implicit=False) + result_triples = db.triples_get( + obj=OBJ_2, + include_implicit=False, + named_graph=named_graph, + ) assert result_triples == [(SUBJ_2, PRED_2, OBJ_2)] # enclosed absolute IRI - result_triples = db.triples_get(obj=f"<{OBJ_2}>", include_implicit=False) + result_triples = db.triples_get( + obj=f"<{OBJ_2}>", + include_implicit=False, + named_graph=named_graph, + ) assert result_triples == [(SUBJ_2, PRED_2, OBJ_2)] # shorthand IRI IRI.add_prefix("ex", "http://example.org/") result_triples = db.triples_get( - obj=f"ex:{utils.get_local_name(OBJ_2)}", include_implicit=False + obj=f"ex:{utils.get_local_name(OBJ_2)}", + include_implicit=False, + named_graph=named_graph, ) assert result_triples == [(SUBJ_2, PRED_2, OBJ_2)] if db.named_graph is not None: # Object as a Python basic type - result_triples = db.triples_get(obj=OBJ_1, include_implicit=False) + result_triples = db.triples_get( + obj=OBJ_1, + include_implicit=False, + named_graph=named_graph, + ) assert result_triples == [(SUB_1, PRED_1, OBJ_1)] # Object as a rdflib Literal result_triples = db.triples_get( - obj=Literal(OBJ_1, datatype=XSD.double), include_implicit=False + obj=Literal(OBJ_1, datatype=XSD.double), + include_implicit=False, + named_graph=named_graph, ) assert result_triples == [(SUB_1, PRED_1, OBJ_1)] From 7c95c1b0a0345996c2e5cfb0d4a7883ac5b670f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Tue, 2 Dec 2025 17:36:01 +0100 Subject: [PATCH 09/36] Bump version to 1.2.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aeed5b5..2d42ddb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "graph-db-interface" -version = "1.1.0" +version = "1.2.0" description = "A simple interface to interact with a GraphDB instance" authors = ["Jan-Felix Klein ", "Nico Brandt ", "Sören Weindel ", "Etienne Hoffmann "] readme = "README.md" From 483f5c389dc14526ebef815f394e42737c691fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Thu, 4 Dec 2025 12:20:40 +0100 Subject: [PATCH 10/36] Add SPARQLQuery support to GraphDB.query - Allows to directly pass SPARQLQuery instances to query method instead of internal resolution --- graph_db_interface/graph_db.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/graph_db_interface/graph_db.py b/graph_db_interface/graph_db.py index bc528f7..4ae0a01 100644 --- a/graph_db_interface/graph_db.py +++ b/graph_db_interface/graph_db.py @@ -8,6 +8,7 @@ from requests import Response from graph_db_interface.utils.graph_db_credentials import GraphDBCredentials from graph_db_interface.utils.iri import IRI +from graph_db_interface.sparql_query import SPARQLQuery from graph_db_interface.exceptions import ( InvalidRepositoryError, AuthenticationError, @@ -280,15 +281,15 @@ def _get_authentication_token( def query( self, - query: str, - update: bool = False, + query: Union[SPARQLQuery, str], + update: Optional[bool] = False, ) -> Optional[Union[Dict, bool]]: """ Execute a SPARQL query or update against the repository. Args: - query (str): The SPARQL query/update string to execute. - update (bool): If True, perform an update; otherwise perform a read query. + query (Union[SPARQLQuery, str]): The SPARQL query/update string to execute. + update (Optional[bool]): If True, perform an update; otherwise perform a read query. Defaults to False. Returns: @@ -297,8 +298,15 @@ def query( failures are handled upstream; otherwise an exception is raised. Raises: + TypeError: If the `query` parameter is neither a `SPARQLQuery` nor a string. + InvalidQueryError: If the SPARQL query is malformed. GraphDbException: If the HTTP request succeeds but the GraphDB API returns an error status. """ + if isinstance(query, SPARQLQuery): + query = query.to_string() + elif not isinstance(query, str): + raise TypeError("Query must be a SPARQLQuery or a string.") + endpoint = f"repositories/{self._repository}" headers = { "Content-Type": "application/sparql-query", From d572ec842ebb461af3463a462ae12a0f2ba46ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Tue, 2 Dec 2025 10:33:50 +0100 Subject: [PATCH 11/36] Add typemap - Custom dict overwriting cast methods from XSD to python methods - Dict mapping XSD to python types --- graph_db_interface/utils/typemap.py | 58 +++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 graph_db_interface/utils/typemap.py diff --git a/graph_db_interface/utils/typemap.py b/graph_db_interface/utils/typemap.py new file mode 100644 index 0000000..069cddb --- /dev/null +++ b/graph_db_interface/utils/typemap.py @@ -0,0 +1,58 @@ +from typing import Any, Callable, Dict, Union +import decimal +import datetime +import rdflib.xsd_datetime +from rdflib.term import XSDToPython, _XSD_PFX, _RDF_XMLLITERAL +from graph_db_interface.utils.iri import IRI +import xml.dom.minidom + +# Extends rdflib.term.XSDToPython +STR_MAP = lambda v: str(v) +IRI_MAP = lambda v: IRI(v) + +XSDToPythonMapper: Dict[IRI, Callable[[str], Any]] = XSDToPython | { + IRI("string", _XSD_PFX): STR_MAP, + IRI("normalizedString", _XSD_PFX): STR_MAP, + IRI("token", _XSD_PFX): STR_MAP, + IRI("language", _XSD_PFX): STR_MAP, + IRI("anyURI", _XSD_PFX): IRI_MAP, + IRI(_RDF_XMLLITERAL): STR_MAP, +} + +XSDToPythonTypes: Dict[IRI, Any] = { + IRI("time", _XSD_PFX): datetime.time, + IRI("date", _XSD_PFX): datetime.date, + IRI("gYear", _XSD_PFX): datetime.date, + IRI("gYearMonth", _XSD_PFX): datetime.date, + IRI("dateTime", _XSD_PFX): datetime.datetime, + IRI("duration", _XSD_PFX): Union[rdflib.xsd_datetime.Duration, datetime.timedelta], + IRI("dayTimeDuration", _XSD_PFX): datetime.timedelta, + IRI("yearMonthDuration", _XSD_PFX): Union[ + rdflib.xsd_datetime.Duration, datetime.timedelta + ], + IRI("hexBinary", _XSD_PFX): bytes, + IRI("string", _XSD_PFX): str, + IRI("normalizedString", _XSD_PFX): str, + IRI("token", _XSD_PFX): str, + IRI("language", _XSD_PFX): str, + IRI("boolean", _XSD_PFX): bool, + IRI("decimal", _XSD_PFX): decimal.Decimal, + IRI("integer", _XSD_PFX): int, + IRI("nonPositiveInteger", _XSD_PFX): int, + IRI("long", _XSD_PFX): int, + IRI("nonNegativeInteger", _XSD_PFX): int, + IRI("negativeInteger", _XSD_PFX): int, + IRI("int", _XSD_PFX): int, + IRI("unsignedLong", _XSD_PFX): int, + IRI("positiveInteger", _XSD_PFX): int, + IRI("short", _XSD_PFX): int, + IRI("unsignedInt", _XSD_PFX): int, + IRI("byte", _XSD_PFX): int, + IRI("unsignedShort", _XSD_PFX): int, + IRI("unsignedByte", _XSD_PFX): int, + IRI("float", _XSD_PFX): float, + IRI("double", _XSD_PFX): float, + IRI("base64Binary", _XSD_PFX): bytes, + IRI("anyURI", _XSD_PFX): IRI, + _RDF_XMLLITERAL: xml.dom.minidom.Document, +} From 4afbc43b8b8f78452bab584418f8b44a2af3194d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 8 Dec 2025 12:43:30 +0100 Subject: [PATCH 12/36] Add IRI lined and onto support, fix process_bindings_select --- graph_db_interface/utils/iri.py | 27 +++++++++++++++++++++++--- graph_db_interface/utils/processing.py | 3 +++ graph_db_interface/utils/typemap.py | 2 +- tests/test_iri.py | 4 ++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/graph_db_interface/utils/iri.py b/graph_db_interface/utils/iri.py index e519a62..0f5af29 100644 --- a/graph_db_interface/utils/iri.py +++ b/graph_db_interface/utils/iri.py @@ -6,6 +6,7 @@ from rdflib import URIRef, Literal from pydantic import GetCoreSchemaHandler from pydantic_core import CoreSchema, core_schema +import regex as re class IRI(URIRef): @@ -34,6 +35,7 @@ class IRI(URIRef): "owl": "http://www.w3.org/2002/07/owl", "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns", "rdfs": "http://www.w3.org/2000/01/rdf-schema", + "xsd": "http://www.w3.org/2001/XMLSchema", "kafka": "http://www.ontotext.com/connectors/kafka", "kafka-inst": "http://www.ontotext.com/connectors/kafka/instance", } @@ -67,9 +69,8 @@ def __new__( iri = IRI._sanitize(value, base, prefixes) return super().__new__(cls, iri) - def short( - self, - ) -> str: + @property + def short(self) -> str: """ Return a prefixed form if the base is registered. @@ -81,6 +82,26 @@ def short( return f"{IRI.PREFIXES_INV[onto]}:{fragment}" return str(self) + @property + def lined(self) -> str: + """ + Return a version of the iri with symbols ".:/#" replaced by "_" + + Returns: + str: Cleaned up name + """ + return "_".join(filter(None, re.split("\.|:|/|#", self))) + + @property + def onto(self) -> str: + """ + Return the ontology/base part of the IRI (before the '#'). + + Returns: + str: The base IRI. + """ + return str(self).rsplit("#", 1)[0] + def __hash__(self) -> int: """ Hash based on the string form to ensure stable behavior in sets/dicts. diff --git a/graph_db_interface/utils/processing.py b/graph_db_interface/utils/processing.py index 7df54b3..c71a720 100644 --- a/graph_db_interface/utils/processing.py +++ b/graph_db_interface/utils/processing.py @@ -60,6 +60,9 @@ def process_bindings_select( return tuple(extract_entry(binding) for binding in bindings) else: # Process with grouping variables + if not bindings: + return {} + assert all( isinstance(bindings[0][key]["value"], Hashable) for key in grouping_variables diff --git a/graph_db_interface/utils/typemap.py b/graph_db_interface/utils/typemap.py index 069cddb..60e0f37 100644 --- a/graph_db_interface/utils/typemap.py +++ b/graph_db_interface/utils/typemap.py @@ -19,7 +19,7 @@ IRI(_RDF_XMLLITERAL): STR_MAP, } -XSDToPythonTypes: Dict[IRI, Any] = { +XSDToPythonTypes: Dict[IRI, type] = { IRI("time", _XSD_PFX): datetime.time, IRI("date", _XSD_PFX): datetime.date, IRI("gYear", _XSD_PFX): datetime.date, diff --git a/tests/test_iri.py b/tests/test_iri.py index 8ece1bc..f4f251c 100644 --- a/tests/test_iri.py +++ b/tests/test_iri.py @@ -306,5 +306,5 @@ def test_short(): iri1 = IRI("http://www.w3.org/2002/07/owl#Class") iri3 = IRI("http://unknown.org#Entity") - assert iri1.short() == "owl:Class" - assert iri3.short() == "http://unknown.org#Entity" + assert iri1.short == "owl:Class" + assert iri3.short == "http://unknown.org#Entity" From d82265c41b91d02a1995baaf2dbe4c0126b5eaaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 8 Dec 2025 13:18:14 +0100 Subject: [PATCH 13/36] Add regex dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 2d42ddb..b541662 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ rdflib = "==7.1.3" requests = "^2.32.3" python-snappy = "^0.7.3" pydantic = "^2.11.7" +regex = "^2024.11.6" [tool.poetry.group.dev.dependencies] black = "^25.1.0" From 9dbfeb9977cf45d311784b72b901469d0e8c5dd0 Mon Sep 17 00:00:00 2001 From: Etienne Hoffmann Date: Mon, 8 Dec 2025 13:24:31 +0100 Subject: [PATCH 14/36] Enhance triples_get to handle BNode types in query results --- graph_db_interface/queries/triple_multi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index f0b9eeb..8c29a94 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -1,7 +1,7 @@ # To be imported into ..graph_db.py GraphDB class from typing import List, Union, Any, Optional, Tuple, TYPE_CHECKING -from rdflib import Literal +from rdflib import Literal, BNode from graph_db_interface.utils import utils from graph_db_interface.utils.iri import IRI from graph_db_interface.utils.utils import Triple, PartialTriple @@ -100,8 +100,8 @@ def _append_bind_and_filter( results = self.query(query=query_string) converted_results = [ ( - IRI(result["s"]["value"]), - IRI(result["p"]["value"]), + IRI(result["s"]["value"]) if result["s"]["type"] == "uri" else BNode(result["s"]["value"]) if result["s"]["type"] == "bnode" else None, + IRI(result["p"]["value"]) if result["p"]["type"] == "uri" else BNode(result["p"]["value"]) if result["p"]["type"] == "bnode" else None, utils.convert_query_result_to_python_type(result["o"]), ) for result in results["results"]["bindings"] From f87f27d683095252de3c6678bd4ac3c105fa2cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 8 Dec 2025 20:52:46 +0100 Subject: [PATCH 15/36] Various - Rename `convert_query_result_to_python_type` to `convert_binding_to_python_type` - Add `convert_bindings` flag to query - Add `convert_multi_bindings_to_python_type` and `shorten_block` methods - Add rdflib.BNode support to `convert_binding_to_python_type` - Fix `Typemap` and `process_bindings_select` --- graph_db_interface/graph_db.py | 20 ++++++- graph_db_interface/queries/triple_multi.py | 22 ++++++-- graph_db_interface/utils/iri.py | 2 +- graph_db_interface/utils/pretty_print.py | 25 +++++++++ graph_db_interface/utils/processing.py | 13 ++--- graph_db_interface/utils/typemap.py | 22 ++++---- graph_db_interface/utils/utils.py | 63 +++++++++++++++++++--- tests/test_utils.py | 6 +-- 8 files changed, 142 insertions(+), 31 deletions(-) diff --git a/graph_db_interface/graph_db.py b/graph_db_interface/graph_db.py index 4ae0a01..8ab32e1 100644 --- a/graph_db_interface/graph_db.py +++ b/graph_db_interface/graph_db.py @@ -14,6 +14,7 @@ AuthenticationError, GraphDbException, ) +from graph_db_interface.utils.utils import convert_multi_bindings_to_python_type class GraphDB: @@ -283,6 +284,7 @@ def query( self, query: Union[SPARQLQuery, str], update: Optional[bool] = False, + convert_bindings: Optional[bool] = False, ) -> Optional[Union[Dict, bool]]: """ Execute a SPARQL query or update against the repository. @@ -291,6 +293,8 @@ def query( query (Union[SPARQLQuery, str]): The SPARQL query/update string to execute. update (Optional[bool]): If True, perform an update; otherwise perform a read query. Defaults to False. + convert_bindings (Optional[bool]): Whether to convert query result bindings to Python types. + Defaults to True. Returns: Optional[Union[Dict, bool]]: If `update` is False, the parsed JSON result dict. @@ -331,4 +335,18 @@ def query( f'Query\n"""\n{query}\n"""\nReturned\n{"Update successful (200)" if update else response.json()}' ) - return True if update else response.json() + if update: + return True + + response = response.json() + + if ( + convert_bindings + and "results" in response + and "bindings" in response["results"] + ): + bindings = response["results"]["bindings"] + converted_bindings = convert_multi_bindings_to_python_type(bindings) + response["results"]["bindings"] = converted_bindings + + return response diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index 8c29a94..c45b25b 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -100,9 +100,25 @@ def _append_bind_and_filter( results = self.query(query=query_string) converted_results = [ ( - IRI(result["s"]["value"]) if result["s"]["type"] == "uri" else BNode(result["s"]["value"]) if result["s"]["type"] == "bnode" else None, - IRI(result["p"]["value"]) if result["p"]["type"] == "uri" else BNode(result["p"]["value"]) if result["p"]["type"] == "bnode" else None, - utils.convert_query_result_to_python_type(result["o"]), + ( + IRI(result["s"]["value"]) + if result["s"]["type"] == "uri" + else ( + BNode(result["s"]["value"]) + if result["s"]["type"] == "bnode" + else None + ) + ), + ( + IRI(result["p"]["value"]) + if result["p"]["type"] == "uri" + else ( + BNode(result["p"]["value"]) + if result["p"]["type"] == "bnode" + else None + ) + ), + utils.convert_binding_to_python_type(result["o"]), ) for result in results["results"]["bindings"] ] diff --git a/graph_db_interface/utils/iri.py b/graph_db_interface/utils/iri.py index 0f5af29..77b4acb 100644 --- a/graph_db_interface/utils/iri.py +++ b/graph_db_interface/utils/iri.py @@ -80,7 +80,7 @@ def short(self) -> str: onto, fragment = str(self).rsplit("#", 1) if onto in IRI.PREFIXES_INV: return f"{IRI.PREFIXES_INV[onto]}:{fragment}" - return str(self) + return self.n3() @property def lined(self) -> str: diff --git a/graph_db_interface/utils/pretty_print.py b/graph_db_interface/utils/pretty_print.py index 7c35408..26e3797 100644 --- a/graph_db_interface/utils/pretty_print.py +++ b/graph_db_interface/utils/pretty_print.py @@ -1,6 +1,31 @@ from __future__ import annotations from typing import Any, Optional import json +from graph_db_interface.utils.iri import IRI +import re + + +def shorten_block(s: str) -> str: + """ + Search a text for IRI patterns with known namespaces and shorten them. + """ + # Pattern to find quoted strings + quote_pattern = re.compile(r'"([^"]*)"') + + def replace_iri(match: re.Match) -> str: + content = match.group(1) + # Skip empty strings + if not content: + return match.group(0) + try: + # Try to create an IRI and get its short form + shortened = IRI(content).short + return f'"{shortened}"' + except Exception: + # If it's not a valid IRI, return the original match + return match.group(0) + + return quote_pattern.sub(replace_iri, s) def format_result( diff --git a/graph_db_interface/utils/processing.py b/graph_db_interface/utils/processing.py index c71a720..dc9a7a4 100644 --- a/graph_db_interface/utils/processing.py +++ b/graph_db_interface/utils/processing.py @@ -1,4 +1,5 @@ from typing import Optional, Any, Hashable +from graph_db_interface.utils.utils import convert_multi_bindings_to_python_type def process_bindings_select( @@ -39,6 +40,7 @@ def process_bindings_select( - `variables` and `grouping_variables` are expected without the leading `?`. - If neither is provided, `variables` are inferred from the first binding when available. """ + bindings = convert_multi_bindings_to_python_type(bindings) if not variables and not grouping_variables: assert bindings, TypeError( @@ -50,10 +52,10 @@ def process_bindings_select( if not variables: extract_entry = lambda binding: None elif len(variables) == 1: - extract_entry = lambda binding: binding[variables[0]]["value"] + extract_entry = lambda binding: binding[variables[0]] else: extract_entry = lambda binding: tuple( - binding[variable]["value"] for variable in variables + binding[variable] for variable in variables ) if not grouping_variables: @@ -64,8 +66,7 @@ def process_bindings_select( return {} assert all( - isinstance(bindings[0][key]["value"], Hashable) - for key in grouping_variables + isinstance(bindings[0][key], Hashable) for key in grouping_variables ), TypeError("All datatypes in grouping_variables must be hashable.") result = {} @@ -74,9 +75,9 @@ def process_bindings_select( for binding in bindings: leaf = result for key in grouping_variables[:-1]: - key_value = binding[key]["value"] + key_value = binding[key] leaf = leaf.setdefault(key_value, {}) - key_value = binding[grouping_variables[-1]]["value"] + key_value = binding[grouping_variables[-1]] leaf.setdefault(key_value, []).append(extract_entry(binding)) leaf_dicts.append(leaf) diff --git a/graph_db_interface/utils/typemap.py b/graph_db_interface/utils/typemap.py index 60e0f37..878ae1e 100644 --- a/graph_db_interface/utils/typemap.py +++ b/graph_db_interface/utils/typemap.py @@ -1,22 +1,22 @@ from typing import Any, Callable, Dict, Union -import decimal import datetime +from rdflib import URIRef import rdflib.xsd_datetime from rdflib.term import XSDToPython, _XSD_PFX, _RDF_XMLLITERAL from graph_db_interface.utils.iri import IRI import xml.dom.minidom # Extends rdflib.term.XSDToPython -STR_MAP = lambda v: str(v) -IRI_MAP = lambda v: IRI(v) -XSDToPythonMapper: Dict[IRI, Callable[[str], Any]] = XSDToPython | { - IRI("string", _XSD_PFX): STR_MAP, - IRI("normalizedString", _XSD_PFX): STR_MAP, - IRI("token", _XSD_PFX): STR_MAP, - IRI("language", _XSD_PFX): STR_MAP, - IRI("anyURI", _XSD_PFX): IRI_MAP, - IRI(_RDF_XMLLITERAL): STR_MAP, +XSDToPythonMapper: Dict[IRI, Callable[[str], Any]] = { + **XSDToPython, + URIRef(_XSD_PFX + "string"): str, + URIRef(_XSD_PFX + "normalizedString"): str, + URIRef(_XSD_PFX + "token"): str, + URIRef(_XSD_PFX + "language"): str, + URIRef(_XSD_PFX + "anyURI"): IRI, + URIRef(_XSD_PFX + "decimal"): float, + URIRef(_RDF_XMLLITERAL): str, } XSDToPythonTypes: Dict[IRI, type] = { @@ -36,7 +36,7 @@ IRI("token", _XSD_PFX): str, IRI("language", _XSD_PFX): str, IRI("boolean", _XSD_PFX): bool, - IRI("decimal", _XSD_PFX): decimal.Decimal, + IRI("decimal", _XSD_PFX): float, IRI("integer", _XSD_PFX): int, IRI("nonPositiveInteger", _XSD_PFX): int, IRI("long", _XSD_PFX): int, diff --git a/graph_db_interface/utils/utils.py b/graph_db_interface/utils/utils.py index 54a1005..ccaf7bd 100644 --- a/graph_db_interface/utils/utils.py +++ b/graph_db_interface/utils/utils.py @@ -1,13 +1,13 @@ import logging from typing import TypeAlias, Union, Optional, Any, Tuple -from rdflib import Literal, XSD, Dataset +from rdflib import Literal, XSD, Dataset, BNode, URIRef from rdflib.plugins.sparql.processor import prepareQuery from graph_db_interface.exceptions import ( InvalidInputError, InvalidQueryError, ) from graph_db_interface.utils.iri import IRI - +from graph_db_interface.utils.typemap import XSDToPythonMapper LOGGER = logging.getLogger(__name__) @@ -197,11 +197,58 @@ def from_xsd_literal( Returns: Any: The converted Python value. """ - literal = Literal(value, datatype=datatype) - return literal.toPython() + return XSDToPythonMapper[URIRef(datatype)](value) + + +def convert_multi_bindings_to_python_type( + bindings: list[dict], +) -> list[dict]: + """ + Convert SPARQL query result bindings to their corresponding Python types. + Args: + bindings (list[dict]): List of SPARQL query result bindings. -def convert_query_result_to_python_type( + Returns: + list[Any]: List of converted Python values. + + Notes: + Bindings are expected in the format + [ + { # binding 1 + 'var1': { 'type': _, 'value': _, 'datatype': _ }, + 'var2': { 'type': _, 'value': _, 'datatype': _ }, + }, + { # binding 2 + 'var1': { 'type': _, 'value': _, 'datatype': _ }, + 'var2': { 'type': _, 'value': _, 'datatype': _ }, + } + ] + and returned in the format + [ + { # binding 1 + 'var1': _, + 'var2': _, + }, + { # binding 2 + 'var1': _, + 'var2': _, + } + ] + """ + converted_bindings = [] + for binding in bindings: + converted_binding = {} + for name, entry in binding.items(): + if isinstance(entry, dict) and "type" in entry and "value" in entry: + converted_binding[name] = convert_binding_to_python_type(entry) + else: + converted_binding[name] = entry + converted_bindings.append(converted_binding) + return converted_bindings + + +def convert_binding_to_python_type( result_binding: dict, ) -> Any: """ @@ -216,8 +263,12 @@ def convert_query_result_to_python_type( type = result_binding.get("type") if type == "literal" and "datatype" in result_binding: return from_xsd_literal(result_binding["value"], result_binding["datatype"]) + elif type == "bnode": + return BNode(result_binding["value"]) elif type == "uri": - return IRI(result_binding["value"]) + # Convert to IRI. if it is a recognized datatype, map to Python type + iri = IRI(result_binding["value"]) + return XSDToPythonMapper.get(iri, iri) else: # If no datatype is provided, return the value as is return result_binding["value"] diff --git a/tests/test_utils.py b/tests/test_utils.py index dda30e5..298b01b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -259,16 +259,16 @@ def test_from_xsd_literal(): assert value == datetime.datetime(2023, 1, 1, 12, 34, 56) -def test_convert_query_result_to_python_type(): +def test_convert_binding_to_python_type(): result_dict = {"type": "uri", "value": "http://example.org#object2"} result_dict2 = { "datatype": "http://www.w3.org/2001/XMLSchema#double", "type": "literal", "value": "0.5", } - converted_result = utils.convert_query_result_to_python_type(result_dict) + converted_result = utils.convert_binding_to_python_type(result_dict) assert converted_result == "http://example.org#object2" - converted_result = utils.convert_query_result_to_python_type(result_dict2) + converted_result = utils.convert_binding_to_python_type(result_dict2) assert converted_result == 0.5 From fb31d14193060de03c8eeedfc0cb3ae35deb2dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 8 Dec 2025 20:55:52 +0100 Subject: [PATCH 16/36] Fix IRI shortening --- tests/test_iri.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_iri.py b/tests/test_iri.py index f4f251c..23396b0 100644 --- a/tests/test_iri.py +++ b/tests/test_iri.py @@ -307,4 +307,4 @@ def test_short(): iri3 = IRI("http://unknown.org#Entity") assert iri1.short == "owl:Class" - assert iri3.short == "http://unknown.org#Entity" + assert iri3.short == "" From c4a37b8860696562cc6bf6470f73d52a2ba4d8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 15 Dec 2025 13:46:31 +0100 Subject: [PATCH 17/36] Add SPARQLQuery classmethods for simple queries - For select, ask, insert_data, insert_exists, delete_data, delete_insert_data queries - Streamlines use of these by reducing required calls - Update internal calls to these methods --- graph_db_interface/kafka/kafka_manager.py | 39 ++++---- .../queries/ontology_helpers.py | 32 ++----- graph_db_interface/queries/triple_multi.py | 67 ++++---------- graph_db_interface/queries/triple_single.py | 40 +++----- graph_db_interface/sparql_query.py | 92 +++++++++++++++++++ 5 files changed, 149 insertions(+), 121 deletions(-) diff --git a/graph_db_interface/kafka/kafka_manager.py b/graph_db_interface/kafka/kafka_manager.py index 8ae1ea6..7662571 100644 --- a/graph_db_interface/kafka/kafka_manager.py +++ b/graph_db_interface/kafka/kafka_manager.py @@ -38,13 +38,12 @@ def get_existing_connector_ids(self) -> List[str]: Raises: GraphDbException: If the underlying query execution fails. """ - query = SPARQLQuery(prefixes=self.db.get_prefixes()) - query.add_select_block( + query = SPARQLQuery.select( variables=["?cntUri", "?cntStr"], where_clauses=["?cntUri kafka:listConnectors ?cntStr ."], + prefixes=self.db.get_prefixes(), ) - query_string = query.to_string(validate=True) - results = self.db.query(query=query_string) + results = self.db.query(query=query) return [res["cntStr"]["value"] for res in results["results"]["bindings"]] def get_status_of_connectors( @@ -66,8 +65,7 @@ def get_status_of_connectors( Raises: GraphDbException: If the underlying query execution fails. """ - query = SPARQLQuery(prefixes=self.db.get_prefixes()) - query.add_select_block( + query = SPARQLQuery.select( variables=["?cntUri", "?cntStr", "?cntStatus"], where_clauses=( ["?cntUri kafka:listConnectors ?cntStr ."] @@ -75,9 +73,9 @@ def get_status_of_connectors( if id is None else [f"kafka-inst:{id} kafka:connectorStatus ?cntStatus ."] ), + prefixes=self.db.get_prefixes(), ) - query_string = query.to_string(validate=True) - results = self.db.query(query=query_string) + results = self.db.query(query=query) if results["results"]["bindings"]: return { res["cntStr"]["value"]: res["cntStatus"]["value"] @@ -105,13 +103,12 @@ def get_connector_create_options( Raises: GraphDbException: If the underlying query execution fails. """ - query = SPARQLQuery(prefixes=self.db.get_prefixes()) - query.add_select_block( + query = SPARQLQuery.select( variables=["?createString"], where_clauses=[f"kafka-inst:{id} kafka:listOptionValues ?createString ."], + prefixes=self.db.get_prefixes(), ) - query_string = query.to_string(validate=True) - results = self.db.query(query=query_string) + results = self.db.query(query=query) if results["results"]["bindings"]: return results["results"]["bindings"][0]["createString"]["value"] return None @@ -129,15 +126,14 @@ def drop_connector( Returns: bool: True on success, False otherwise. """ - query = SPARQLQuery(prefixes=self.db.get_prefixes()) - query.add_insert_data_block( + query = SPARQLQuery.insert_data( triples=[ (f"kafka-inst:{id}", "kafka:dropConnector", "[]"), - ] + ], + prefixes=self.db.get_prefixes(), ) - query_string = query.to_string(validate=False) try: - self.db.query(query=query_string, update=True) + self.db.query(query=query, update=True) self.logger.info(f"Dropped Kafka connector with ID: {id}") return True except Exception as e: @@ -166,19 +162,18 @@ def create_connector( if overwrite and id in self.get_existing_connector_ids(): self.drop_connector(id) - query = SPARQLQuery(prefixes=self.db.get_prefixes()) - query.add_insert_data_block( + query = SPARQLQuery.insert_data( triples=[ ( f"kafka-inst:{id}", "kafka:createConnector", f"'''{json.dumps(connector_config, indent=2)}'''", ), - ] + ], + prefixes=self.db.get_prefixes(), ) - query_string = query.to_string(validate=False) try: - self.db.query(query=query_string, update=True) + self.db.query(query=query, update=True) self.logger.info(f"Created Kafka connector with ID: {id}") except Exception as e: self.logger.error( diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index 76d2be4..53dc250 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -58,22 +58,14 @@ def iri_exists( where_clauses.append(f"{{?s {iri.n3()} ?o . }}") if as_obj: where_clauses.append(f"{{?s ?p {iri.n3()} . }}") - query = SPARQLQuery( + + query = SPARQLQuery.ask( + where_clauses=where_clauses, named_graph=named_graph, include_explicit=include_explicit, include_implicit=include_implicit, ) - - query.add_ask_block( - where_clauses=where_clauses, - ) - - query_string = query.to_string(validate=True) - - result = self.query( - query=query_string, - update=False, - ) + result = self.query(query=query, update=False) if result is not None and result["boolean"]: self.logger.debug(f"Found IRI {iri}") return True @@ -177,24 +169,18 @@ def owl_get_classes_of_individual( else: filter_conditions = "" - query = SPARQLQuery( - named_graph=named_graph, - include_explicit=include_explicit, - include_implicit=include_implicit, - ) - - query.add_select_block( + query = SPARQLQuery.select( variables=["?class"], where_clauses=[ f"?class rdf:type owl:Class .", f"{instance_iri.n3()} rdf:type ?class .", filter_conditions, ], + named_graph=named_graph, + include_explicit=include_explicit, + include_implicit=include_implicit, ) - - query_string = query.to_string(validate=True) - - results = self.query(query=query_string) + results = self.query(query=query) if results is None: return [] diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index c45b25b..ddd7469 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -81,23 +81,14 @@ def _append_bind_and_filter( if obj is not None: _append_bind_and_filter("?o", obj) - query = SPARQLQuery( + query = SPARQLQuery.select( + variables=["?s", "?p", "?o"], + where_clauses=binds + ["?s ?p ?o ."] + filter, named_graph=named_graph, include_explicit=include_explicit, include_implicit=include_implicit, ) - query.add_select_block( - variables=["?s", "?p", "?o"], - where_clauses=binds + ["?s ?p ?o ."] + filter, - ) - query_string = query.to_string(validate=True) - if query_string is None: - self.logger.error( - "Unable to construct SPARQL query, returning empty list of triples" - ) - return [] - - results = self.query(query=query_string) + results = self.query(query=query) converted_results = [ ( ( @@ -185,15 +176,11 @@ def all_triple_exists( triple = utils.sanitize_triple(triple) triple_strings.append(utils.triple_to_string(triple, ".")) - query = SPARQLQuery(named_graph=named_graph) - query.add_ask_block(where_clauses=triple_strings) - query_string = query.to_string() - if not query_string: - raise InvalidInputError( - f"Could not generate 'all_triple_exists' query for triples ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" - ) - - ask_result = self.query(query=query_string, update=False) + query = SPARQLQuery.ask( + where_clauses=triple_strings, + named_graph=named_graph, + ) + ask_result = self.query(query=query, update=False) if ask_result is None: raise InvalidInputError( f"Could not query 'all_triple_exists' for triples ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" @@ -249,17 +236,11 @@ def triples_add( utils.triple_to_string(triple, ".") for triple in validated_triples_to_add ] - query = SPARQLQuery(named_graph=named_graph) - query.add_insert_data_block( + query = SPARQLQuery.insert_data( triples=validated_triples_to_add, + named_graph=named_graph, ) - query_string = query.to_string() - if not query_string: - raise InvalidInputError( - f"Could not generate 'triples_add' query for triples ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" - ) - - result = self.query(query=query_string, update=True) + result = self.query(query=query, update=True) if not result: self.logger.warning( f"Failed to add triples: ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" @@ -310,17 +291,11 @@ def triples_delete( utils.triple_to_string(triple, ".") for triple in validated_triples_to_delete ] - query = SPARQLQuery(named_graph=named_graph) - query.add_delete_data_block( + query = SPARQLQuery.delete_data( triples=validated_triples_to_delete, + named_graph=named_graph, ) - query_string = query.to_string() - if not query_string: - raise InvalidInputError( - f"Could not generate 'triples_delete' query for triples ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" - ) - - result = self.query(query=query_string, update=True) + result = self.query(query=query, update=True) if not result: self.logger.warning( f"Failed to delete triples: ({triple_strings}), named_graph: {named_graph or "default"}, repository: {self._repository}" @@ -375,19 +350,13 @@ def triples_update( utils.triple_to_string(triple, ".") for triple in validated_old_triples ] - query = SPARQLQuery(named_graph=named_graph) - query.add_delete_insert_data_block( + query = SPARQLQuery.delete_insert_data( delete_triples=validated_old_triples, insert_triples=validated_new_triples, where_clauses=old_triple_strings, + named_graph=named_graph, ) - query_string = query.to_string(validate=True) - if not query_string: - raise InvalidInputError( - f"Could not generate 'triples_update' query for triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or "default"}, repository: {self._repository}" - ) - - result = self.query(query=query_string, update=True) + result = self.query(query=query, update=True) if not result: self.logger.warning( f"Failed to update triples ({validated_old_triples}) -> ({validated_new_triples}), named_graph: {named_graph or "default"}, repository: {self._repository}" diff --git a/graph_db_interface/queries/triple_single.py b/graph_db_interface/queries/triple_single.py index 5231765..d8f02c8 100644 --- a/graph_db_interface/queries/triple_single.py +++ b/graph_db_interface/queries/triple_single.py @@ -33,15 +33,13 @@ def triple_exists( triple = utils.sanitize_triple(triple) named_graph = IRI(named_graph) if named_graph is not None else self.named_graph - query = SPARQLQuery(named_graph=named_graph) - query.add_ask_block( + query = SPARQLQuery.ask( where_clauses=[ utils.triple_to_string(triple, "."), ], + named_graph=named_graph, ) - query_string = query.to_string() - - result = self.query(query=query_string) + result = self.query(query=query) if result is None or result["boolean"] is False: self.logger.debug( f"Unable to find triple ({utils.triple_to_string(triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" @@ -72,15 +70,11 @@ def triple_add( triple = utils.sanitize_triple(triple) named_graph = IRI(named_graph) if named_graph is not None else self.named_graph - query = SPARQLQuery(named_graph=named_graph) - query.add_insert_data_block( + query = SPARQLQuery.insert_data( triples=[triple], + named_graph=named_graph, ) - query_string = query.to_string() - if query_string is None: - return False - - result = self.query(query=query_string, update=True) + result = self.query(query=query, update=True) if not result: self.logger.warning( f"Failed to insert triple: ({utils.triple_to_string(triple)}) named_graph: {named_graph or "default"}, repository: {self._repository}" @@ -122,16 +116,12 @@ def triple_delete( if not self.triple_exists(triple, named_graph=named_graph): self.logger.warning("Unable to delete triple since it does not exist") return False - query = SPARQLQuery(named_graph=named_graph) - query.add_delete_data_block( + + query = SPARQLQuery.delete_data( triples=[triple], + named_graph=named_graph, ) - query_string = query.to_string() - - if query_string is None: - return False - - result = self.query(query=query_string, update=True) + result = self.query(query=query, update=True) if not result: self.logger.warning( f"Failed to delete triple: ({utils.triple_to_string(triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" @@ -201,17 +191,13 @@ def triple_update( # Determine replacement variables update_triple = tuple(n if n else o for o, n in zip(old_triple, new_triple)) - query = SPARQLQuery(named_graph=named_graph) - query.add_delete_insert_data_block( + query = SPARQLQuery.delete_insert_data( delete_triples=[old_triple], insert_triples=[update_triple], where_clauses=[utils.triple_to_string(old_triple, ".")], + named_graph=named_graph, ) - query_string = query.to_string(validate=True) - if query_string is None: - return False - - result = self.query(query=query_string, update=True) + result = self.query(query=query, update=True) if not result: self.logger.warning( f"Failed to update triple to: ({utils.triple_to_string(update_triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" diff --git a/graph_db_interface/sparql_query.py b/graph_db_interface/sparql_query.py index 584c3a4..b4325d7 100644 --- a/graph_db_interface/sparql_query.py +++ b/graph_db_interface/sparql_query.py @@ -39,6 +39,22 @@ def __init__( self._include_implicit = include_implicit self._query_blocks = [] + @classmethod + def select( + cls, + variables: List[str], + where_clauses: List[str], + select_type: Optional[SPARQLQueryType] = SPARQLQueryType.SELECT, + **kwargs, + ) -> "SPARQLQuery": + """ + Create a new SPARQLQuery instance with a SELECT block. + Refer to `add_select_block` for details. + """ + query = cls(**kwargs) + query.add_select_block(variables, where_clauses, select_type) + return query + def add_select_block( self, variables: List[str], @@ -66,6 +82,20 @@ def add_select_block( block = "\n".join(block_parts) self._query_blocks.append({"type": select_type, "data": block}) + @classmethod + def ask( + cls, + where_clauses: List[str], + **kwargs, + ) -> "SPARQLQuery": + """ + Create a new SPARQLQuery instance with a ASK block. + Refer to `add_ask_block` for details. + """ + query = cls(**kwargs) + query.add_ask_block(where_clauses) + return query + def add_ask_block( self, where_clauses: List[str], @@ -91,6 +121,20 @@ def add_ask_block( block = "\n".join(block_parts) self._query_blocks.append({"type": SPARQLQueryType.ASK, "data": block}) + @classmethod + def insert_data( + cls, + triples: List[Triple], + **kwargs, + ) -> "SPARQLQuery": + """ + Create a new SPARQLQuery instance with an INSERT DATA block. + Refer to `add_insert_data_block` for details. + """ + query = cls(**kwargs) + query.add_insert_data_block(triples) + return query + def add_insert_data_block( self, triples: List[Triple], @@ -115,6 +159,20 @@ def add_insert_data_block( block = "\n".join(block_parts) self._query_blocks.append({"type": SPARQLQueryType.INSERT_DATA, "data": block}) + @classmethod + def insert_exists( + cls, + triples: List[Triple], + **kwargs, + ) -> "SPARQLQuery": + """ + Create a new SPARQLQuery instance with an INSERT ... WHERE NOT EXISTS block. + Refer to `add_insert_exists_block` for details. + """ + query = cls(**kwargs) + query.add_insert_exists_block(triples) + return query + def add_insert_exists_block( self, triples: List[Triple], @@ -144,6 +202,20 @@ def add_insert_exists_block( {"type": SPARQLQueryType.INSERT_EXISTS, "data": block} ) + @classmethod + def delete_data( + cls, + triples: List[Triple], + **kwargs, + ) -> "SPARQLQuery": + """ + Create a new SPARQLQuery instance with a DELETE DATA block. + Refer to `add_delete_data_block` for details. + """ + query = cls(**kwargs) + query.add_delete_data_block(triples) + return query + def add_delete_data_block( self, triples: List[Triple], @@ -168,6 +240,26 @@ def add_delete_data_block( block = "\n".join(block_parts) self._query_blocks.append({"type": SPARQLQueryType.DELETE_DATA, "data": block}) + @classmethod + def delete_insert_data( + cls, + delete_triples: List[Triple], + insert_triples: List[Triple], + where_clauses: List[str], + **kwargs, + ) -> "SPARQLQuery": + """ + Create a new SPARQLQuery instance with a DELETE DATA block. + Refer to `add_delete_insert_data_block` for details. + """ + query = cls(**kwargs) + query.add_delete_insert_data_block( + delete_triples, + insert_triples, + where_clauses, + ) + return query + def add_delete_insert_data_block( self, delete_triples: List[Triple], From 06d32dd9c141f2ddf29f39ced8d13deac8a63e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 15 Dec 2025 14:23:34 +0100 Subject: [PATCH 18/36] Add new IRI and blank id generation - `GraphDB.new_iri` generates a new random IRI, ensuring it does not exist in the database - `GraphDB.new_blank_id` generates new a random genid str, ensuring that it does not exist in the context of the GraphDB instance --- graph_db_interface/graph_db.py | 3 + .../queries/ontology_helpers.py | 58 +++++++++++++++- tests/test_graph_manipulation.py | 67 +++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) diff --git a/graph_db_interface/graph_db.py b/graph_db_interface/graph_db.py index 8ab32e1..634f8a5 100644 --- a/graph_db_interface/graph_db.py +++ b/graph_db_interface/graph_db.py @@ -41,6 +41,7 @@ def __init__( self._credentials = credentials self._timeout = timeout self._auth = None + self._blank_ids = set() if use_gdb_token: self._auth = self._get_authentication_token( @@ -101,6 +102,8 @@ def from_env( ) from graph_db_interface.queries.ontology_helpers import ( iri_exists, + new_iri, + new_blank_id, is_subclass, owl_is_named_individual, owl_get_classes_of_individual, diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index 53dc250..6b57a79 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -1,6 +1,7 @@ # To be imported into ..graph_db.py GraphDB class +import uuid -from typing import List, Optional, Union, TYPE_CHECKING +from typing import List, Optional, Union, Callable, TYPE_CHECKING from graph_db_interface.utils import utils from graph_db_interface.utils.iri import IRI from graph_db_interface.exceptions import InvalidInputError @@ -74,6 +75,61 @@ def iri_exists( return False +def new_iri( + self: "GraphDB", + base: Union[str, IRI], + schema: Optional[Callable[[], str]] = lambda: str(uuid.uuid4()), +) -> IRI: + """ + Generate a new unique IRI within the graph database's namespace. + + Args: + base (Union[str, IRI]): The base IRI or namespace for the new IRI. + schema (Optional[Callable[[], str]]): A callable that generates the unique part + of the IRI. Defaults to a UUID4 string generator. + + Returns: + IRI: A new unique IRI. + """ + if schema() == schema(): + raise ValueError("Schema function must produce different values on each call") + + def new() -> str: + return IRI(value=schema(), base=base) + + iri = new() + + while self.iri_exists(iri, as_sub=True, as_pred=True, as_obj=True): + iri = new() + return iri + + +def new_blank_id( + self: "GraphDB", + schema: Optional[Callable[[], str]] = lambda: f"genid-{uuid.uuid4()}", +) -> str: + """ + Generate a new unique blank node identifier. + + Args: + schema (Optional[Callable[[], str]]): A callable that generates the blank node ID. + Defaults to a `genid-` format. + + Returns: + str: A new unique blank node identifier. + """ + if schema() == schema(): + raise ValueError("Schema function must produce different values on each call") + + genid = schema() + + while genid in self._blank_ids: + genid = schema() + + self._blank_ids.add(genid) + return genid + + def is_subclass( self: "GraphDB", subclass_iri: Union[str, IRI], diff --git a/tests/test_graph_manipulation.py b/tests/test_graph_manipulation.py index 566f610..bd16007 100644 --- a/tests/test_graph_manipulation.py +++ b/tests/test_graph_manipulation.py @@ -1,8 +1,10 @@ +import random import pytest from rdflib import Literal, XSD from graph_db_interface import GraphDB from graph_db_interface.exceptions import InvalidInputError from graph_db_interface.utils import utils +from graph_db_interface.utils.iri import IRI GLOBAL_NAMED_GRAPH = "http://example.org/named_graph" LOCAL_NAMED_GRAPH = "http://example.org/local_named_graph" @@ -599,3 +601,68 @@ def test_convenience_functions(db: GraphDB, named_graph: str): named_graph=named_graph, ) assert result is True + + +def test_iri_generation(db: GraphDB, named_graph: str): + valid_schema = lambda: str(random.randint(0, 1000000)) + invalid_schema = lambda: "fixed" + + # Generate a new IRI + iri1 = db.new_iri( + base="http://example.org", + ) + assert isinstance(iri1, IRI) + assert iri1.onto == "http://example.org" + + # Ensure uniqueness + iri2 = db.new_iri( + base="http://example.org", + ) + assert iri1 != iri2 + + # Generate with schema + iri3 = db.new_iri( + base="http://example.org", + schema=valid_schema, + ) + assert isinstance(iri3, IRI) + assert iri3.onto == "http://example.org" + assert iri1 != iri3 + + iri4 = db.new_iri( + base="http://example.org", + schema=valid_schema, + ) + assert iri3 != iri4 + + # Invalid schema that does not produce unique IRIs + with pytest.raises(ValueError): + db.new_iri( + base="http://example.org", + schema=invalid_schema, + ) + + # Generate blank node IDs + genid1 = db.new_blank_id() + assert isinstance(genid1, str) + + genid2 = db.new_blank_id() + assert genid1 != genid2 + + # Generate with schema + genid3 = db.new_blank_id( + schema=valid_schema, + ) + assert isinstance(genid3, str) + assert genid1 != genid3 + + genid4 = db.new_blank_id( + schema=valid_schema, + ) + assert genid3 != genid4 + + # Invalid schema that does not produce unique blank IDs + with pytest.raises(ValueError): + db.new_blank_id( + schema=invalid_schema, + ) From 5842b216e66ff8918d7a17e4ba5065c56c4f841d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 15 Dec 2025 14:24:24 +0100 Subject: [PATCH 19/36] Add sanitize_triple partial test --- tests/test_utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 298b01b..1655902 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -161,6 +161,14 @@ def test_sanitize_triple_partial(): with pytest.raises(TypeError): utils.sanitize_triple(partial_triple_invalid, allow_partial=True) + partial_triple_two_entries = ( + IRI("http://example.org#subject"), + IRI("http://example.org#predicate"), + ) + # only 2 elements + with pytest.raises(InvalidInputError): + utils.sanitize_triple(partial_triple_two_entries) + def test_validate_query(): valid_query = """ From 23ada50f0012ee990c37e37dfd43e9ac25446ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 15 Dec 2025 20:42:35 +0100 Subject: [PATCH 20/36] Add XSDToPythonTypes and XSDToPythonMapper to init and __all__ --- graph_db_interface/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graph_db_interface/__init__.py b/graph_db_interface/__init__.py index eb9c735..4b2fec8 100644 --- a/graph_db_interface/__init__.py +++ b/graph_db_interface/__init__.py @@ -6,6 +6,7 @@ from .utils.processing import process_bindings_select from .utils.pretty_print import format_result from .kafka.kafka_manager import KafkaManager +from .utils.typemap import XSDToPythonTypes, XSDToPythonMapper __all__ = [ "GraphDB", @@ -16,4 +17,6 @@ "process_bindings_select", "format_result", "KafkaManager", + "XSDToPythonTypes", + "XSDToPythonMapper", ] From 1637b5efeab7d2d0134df00cbcea3a734c488f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Tue, 16 Dec 2025 14:54:35 +0100 Subject: [PATCH 21/36] Allow fragment in base of randomly generated IRI --- .../queries/ontology_helpers.py | 8 ++++++- tests/test_graph_manipulation.py | 24 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index 6b57a79..1bd2cca 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -94,8 +94,14 @@ def new_iri( if schema() == schema(): raise ValueError("Schema function must produce different values on each call") + base = base.rstrip("#") + if "#" in base: + base, fragment_start = base.split("#", 1) + else: + fragment_start = "" + def new() -> str: - return IRI(value=schema(), base=base) + return IRI(value=fragment_start + schema(), base=base) iri = new() diff --git a/tests/test_graph_manipulation.py b/tests/test_graph_manipulation.py index bd16007..23e6470 100644 --- a/tests/test_graph_manipulation.py +++ b/tests/test_graph_manipulation.py @@ -620,20 +620,34 @@ def test_iri_generation(db: GraphDB, named_graph: str): ) assert iri1 != iri2 - # Generate with schema + # Generate with fragment base iri3 = db.new_iri( - base="http://example.org", - schema=valid_schema, + base="http://example.org#ClassName_", ) assert isinstance(iri3, IRI) assert iri3.onto == "http://example.org" - assert iri1 != iri3 + assert iri3.fragment.startswith("ClassName_") + # Ensure uniqueness iri4 = db.new_iri( + base="http://example.org#ClassName_", + ) + assert iri3 != iri4 + + # Generate with schema + iri5 = db.new_iri( base="http://example.org", schema=valid_schema, ) - assert iri3 != iri4 + assert isinstance(iri5, IRI) + assert iri5.onto == "http://example.org" + assert iri1 != iri5 + + iri6 = db.new_iri( + base="http://example.org", + schema=valid_schema, + ) + assert iri5 != iri6 # Invalid schema that does not produce unique IRIs with pytest.raises(ValueError): From 19a868949550215e09f74eb776fddd73430d9041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Tue, 16 Dec 2025 15:15:54 +0100 Subject: [PATCH 22/36] Expose Triple and PartialTriple TypeAlias --- graph_db_interface/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/graph_db_interface/__init__.py b/graph_db_interface/__init__.py index 4b2fec8..447361c 100644 --- a/graph_db_interface/__init__.py +++ b/graph_db_interface/__init__.py @@ -2,7 +2,7 @@ from .graph_db import GraphDB from .utils.graph_db_credentials import GraphDBCredentials from .utils.iri import IRI -from .utils.utils import to_literal +from .utils.utils import Triple, PartialTriple, to_literal from .utils.processing import process_bindings_select from .utils.pretty_print import format_result from .kafka.kafka_manager import KafkaManager @@ -13,6 +13,8 @@ "GraphDBCredentials", "IRI", "SPARQLQuery", + "Triple", + "PartialTriple", "to_literal", "process_bindings_select", "format_result", From 79fad59c2fc43be6fe3fb9ab097ec67bd7e42201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Tue, 16 Dec 2025 17:37:26 +0100 Subject: [PATCH 23/36] Switch result type conversion to query convert_bindings handler --- graph_db_interface/queries/named_graph.py | 6 ++--- .../queries/ontology_helpers.py | 8 +++--- graph_db_interface/queries/triple_multi.py | 26 +++---------------- 3 files changed, 9 insertions(+), 31 deletions(-) diff --git a/graph_db_interface/queries/named_graph.py b/graph_db_interface/queries/named_graph.py index 3211f26..7a679f4 100644 --- a/graph_db_interface/queries/named_graph.py +++ b/graph_db_interface/queries/named_graph.py @@ -27,9 +27,7 @@ def get_list_of_named_graphs( GRAPH ?graph { ?s ?p ?o } } """ - results = self.query(query) - + results = self.query(query, convert_bindings=True) if results is None: return [] - - return [IRI(result["graph"]["value"]) for result in results["results"]["bindings"]] + return [result["graph"] for result in results["results"]["bindings"]] diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index 1bd2cca..5ea613f 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -242,19 +242,17 @@ def owl_get_classes_of_individual( include_explicit=include_explicit, include_implicit=include_implicit, ) - results = self.query(query=query) + results = self.query(query=query, convert_bindings=True) if results is None: return [] if local_name: classes = [ - utils.get_local_name(result["class"]["value"]) + utils.get_local_name(result["class"]) for result in results["results"]["bindings"] ] return classes - classes = [ - IRI(result["class"]["value"]) for result in results["results"]["bindings"] - ] + classes = [result["class"] for result in results["results"]["bindings"]] return classes diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index ddd7469..cf07aca 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -88,31 +88,13 @@ def _append_bind_and_filter( include_explicit=include_explicit, include_implicit=include_implicit, ) - results = self.query(query=query) + results = self.query(query=query, convert_bindings=True) + converted_results = [ - ( - ( - IRI(result["s"]["value"]) - if result["s"]["type"] == "uri" - else ( - BNode(result["s"]["value"]) - if result["s"]["type"] == "bnode" - else None - ) - ), - ( - IRI(result["p"]["value"]) - if result["p"]["type"] == "uri" - else ( - BNode(result["p"]["value"]) - if result["p"]["type"] == "bnode" - else None - ) - ), - utils.convert_binding_to_python_type(result["o"]), - ) + (result["s"], result["p"], result["o"]) for result in results["results"]["bindings"] ] + return converted_results From 89386c6725e9130074a2ef7362b7e1630bb23683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Wed, 7 Jan 2026 17:35:17 +0100 Subject: [PATCH 24/36] Fix bug in convert_binding_to_python_type - TODO needs more testing to catch this bug --- graph_db_interface/utils/utils.py | 4 ++-- tests/test_utils.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/graph_db_interface/utils/utils.py b/graph_db_interface/utils/utils.py index ccaf7bd..1e2c8b6 100644 --- a/graph_db_interface/utils/utils.py +++ b/graph_db_interface/utils/utils.py @@ -7,7 +7,7 @@ InvalidQueryError, ) from graph_db_interface.utils.iri import IRI -from graph_db_interface.utils.typemap import XSDToPythonMapper +from graph_db_interface.utils.typemap import XSDToPythonMapper, XSDToPythonTypes LOGGER = logging.getLogger(__name__) @@ -268,7 +268,7 @@ def convert_binding_to_python_type( elif type == "uri": # Convert to IRI. if it is a recognized datatype, map to Python type iri = IRI(result_binding["value"]) - return XSDToPythonMapper.get(iri, iri) + return XSDToPythonTypes.get(iri, iri) else: # If no datatype is provided, return the value as is return result_binding["value"] diff --git a/tests/test_utils.py b/tests/test_utils.py index 1655902..a61382c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -278,6 +278,7 @@ def test_convert_binding_to_python_type(): assert converted_result == "http://example.org#object2" converted_result = utils.convert_binding_to_python_type(result_dict2) assert converted_result == 0.5 + # TODO add more test cases here to catch last modification def test_get_local_name(): From d0ca6352dc2e9dc534799599f96a9f0f627552b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Fri, 9 Jan 2026 20:18:42 +0100 Subject: [PATCH 25/36] Add verbose types in types.py, Add support for BNodes in sanitize methods --- graph_db_interface/__init__.py | 4 +- graph_db_interface/graph_db.py | 7 +- graph_db_interface/queries/named_graph.py | 4 +- .../queries/ontology_helpers.py | 41 +++---- .../queries/rdf4j/graph_store.py | 15 +-- graph_db_interface/queries/triple_multi.py | 85 +++++++------ graph_db_interface/queries/triple_single.py | 60 +++++----- graph_db_interface/sparql_query.py | 36 +++--- graph_db_interface/utils/types.py | 41 +++++++ graph_db_interface/utils/utils.py | 113 +++++++++++------- tests/test_utils.py | 48 ++++++-- 11 files changed, 283 insertions(+), 171 deletions(-) create mode 100644 graph_db_interface/utils/types.py diff --git a/graph_db_interface/__init__.py b/graph_db_interface/__init__.py index 447361c..4b2fec8 100644 --- a/graph_db_interface/__init__.py +++ b/graph_db_interface/__init__.py @@ -2,7 +2,7 @@ from .graph_db import GraphDB from .utils.graph_db_credentials import GraphDBCredentials from .utils.iri import IRI -from .utils.utils import Triple, PartialTriple, to_literal +from .utils.utils import to_literal from .utils.processing import process_bindings_select from .utils.pretty_print import format_result from .kafka.kafka_manager import KafkaManager @@ -13,8 +13,6 @@ "GraphDBCredentials", "IRI", "SPARQLQuery", - "Triple", - "PartialTriple", "to_literal", "process_bindings_select", "format_result", diff --git a/graph_db_interface/graph_db.py b/graph_db_interface/graph_db.py index 634f8a5..aefd197 100644 --- a/graph_db_interface/graph_db.py +++ b/graph_db_interface/graph_db.py @@ -8,6 +8,7 @@ from requests import Response from graph_db_interface.utils.graph_db_credentials import GraphDBCredentials from graph_db_interface.utils.iri import IRI +from graph_db_interface.utils.types import GraphNameLike, GraphName from graph_db_interface.sparql_query import SPARQLQuery from graph_db_interface.exceptions import ( InvalidRepositoryError, @@ -31,7 +32,7 @@ def __init__( credentials: GraphDBCredentials, timeout: int = 60, use_gdb_token: bool = True, - named_graph: Optional[Union[str, IRI]] = None, + named_graph: Optional[GraphNameLike] = None, logger: Optional[logging.Logger] = None, ): if logger is None: @@ -127,7 +128,7 @@ def repository( self._repository = self._validate_repository(value) @property - def named_graph(self) -> Optional[IRI]: + def named_graph(self) -> Optional[GraphName]: """ The currently selected default named graph. @@ -139,7 +140,7 @@ def named_graph(self) -> Optional[IRI]: @named_graph.setter def named_graph( self, - value: Optional[Union[str, IRI]], + value: Optional[GraphNameLike], ): if value is None: self._named_graph = None diff --git a/graph_db_interface/queries/named_graph.py b/graph_db_interface/queries/named_graph.py index 7a679f4..b40e365 100644 --- a/graph_db_interface/queries/named_graph.py +++ b/graph_db_interface/queries/named_graph.py @@ -1,7 +1,7 @@ # To be imported into ..graph_db.py GraphDB class from typing import List, TYPE_CHECKING -from graph_db_interface.utils.iri import IRI +from graph_db_interface.utils.types import GraphNameLike if TYPE_CHECKING: from graph_db_interface import GraphDB @@ -9,7 +9,7 @@ def get_list_of_named_graphs( self: "GraphDB", -) -> List[IRI]: +) -> List[GraphNameLike]: """ Get the list of named graphs in the current repository. diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index 5ea613f..bb4fcef 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -4,6 +4,7 @@ from typing import List, Optional, Union, Callable, TYPE_CHECKING from graph_db_interface.utils import utils from graph_db_interface.utils.iri import IRI +from graph_db_interface.utils.types import IRILike, GraphNameLike from graph_db_interface.exceptions import InvalidInputError from rdflib import Namespace @@ -15,25 +16,25 @@ def iri_exists( self: "GraphDB", - iri: Union[str, IRI], + iri: IRILike, as_sub: Optional[bool] = False, as_pred: Optional[bool] = False, as_obj: Optional[bool] = False, include_explicit: Optional[bool] = True, include_implicit: Optional[bool] = True, - named_graph: Optional[Union[str, IRI]] = None, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Check if an IRI exists as subject, predicate, or object. Args: - iri (Union[str, IRI]): The IRI to check for existence. + iri (IRILike): The IRI to check for existence. as_sub (Optional[bool]): If True, check existence as subject. Defaults to False. as_pred (Optional[bool]): If True, check existence as predicate. Defaults to False. as_obj (Optional[bool]): If True, check existence as object. Defaults to False. include_explicit (Optional[bool]): Include explicit triples (`FROM onto:explicit`). Defaults to True. include_implicit (Optional[bool]): Include inferred triples (`FROM onto:implicit`). Defaults to True. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if the IRI exists based on the specified criteria, False otherwise. @@ -77,14 +78,14 @@ def iri_exists( def new_iri( self: "GraphDB", - base: Union[str, IRI], + base: IRILike, schema: Optional[Callable[[], str]] = lambda: str(uuid.uuid4()), ) -> IRI: """ Generate a new unique IRI within the graph database's namespace. Args: - base (Union[str, IRI]): The base IRI or namespace for the new IRI. + base (IRILike): The base IRI or namespace for the new IRI. schema (Optional[Callable[[], str]]): A callable that generates the unique part of the IRI. Defaults to a UUID4 string generator. @@ -138,9 +139,9 @@ def new_blank_id( def is_subclass( self: "GraphDB", - subclass_iri: Union[str, IRI], - class_iri: Union[str, IRI], - named_graph: Optional[Union[str, IRI]] = None, + subclass_iri: IRILike, + class_iri: IRILike, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Check whether one class is a subclass of another (`rdfs:subClassOf`). @@ -148,9 +149,9 @@ def is_subclass( Asks for `subclass_iri rdfs:subClassOf class_iri` Args: - subclass_iri (Union[str, IRI]): The IRI of the potential subclass. - class_iri (Union[str, IRI]): The IRI of the potential superclass. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + subclass_iri (IRILike): The IRI of the potential subclass. + class_iri (IRILike): The IRI of the potential superclass. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if `subclass_iri` is a subclass of `class_iri`, False otherwise. @@ -163,8 +164,8 @@ def is_subclass( def owl_is_named_individual( self: "GraphDB", - iri: Union[str, IRI], - named_graph: Optional[Union[str, IRI]] = None, + iri: IRILike, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Check if the given IRI corresponds to an OWL named individual. @@ -172,8 +173,8 @@ def owl_is_named_individual( Asks for `iri rdf:type owl:NamedIndividual`. Args: - iri (Union[str, IRI]): The IRI to check. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + iri (IRILike): The IRI to check. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if the IRI is a named individual, False otherwise. @@ -186,12 +187,12 @@ def owl_is_named_individual( def owl_get_classes_of_individual( self: "GraphDB", - instance_iri: Union[str, IRI], + instance_iri: IRILike, ignored_prefixes: Optional[List[Namespace]] = None, local_name: Optional[bool] = False, include_explicit: Optional[bool] = True, include_implicit: Optional[bool] = False, - named_graph: Optional[Union[str, IRI]] = None, + named_graph: Optional[GraphNameLike] = None, ) -> List[Union[IRI, str]]: """ Get the OWL classes associated with a given individual. @@ -200,13 +201,13 @@ def owl_get_classes_of_individual( optionally filters out results by prefix or returns local names only. Args: - instance_iri (Union[str, IRI]): IRI of the individual to inspect. + instance_iri (IRILike): IRI of the individual to inspect. ignored_prefixes (Optional[List[Namespace]]): Prefixes/namespaces to ignore when collecting classes. Defaults to ["owl", "rdfs"]. local_name (Optional[bool]): If True, return only the local names of the classes. Defaults to False. include_explicit (Optional[bool]): Include explicit triples. Defaults to True. include_implicit (Optional[bool]): Include inferred triples. Defaults to False. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: List[Union[IRI, str]]: Class IRIs, or local names if `local_name=True`. diff --git a/graph_db_interface/queries/rdf4j/graph_store.py b/graph_db_interface/queries/rdf4j/graph_store.py index 78462ec..d2f5a20 100644 --- a/graph_db_interface/queries/rdf4j/graph_store.py +++ b/graph_db_interface/queries/rdf4j/graph_store.py @@ -1,7 +1,8 @@ from requests import Response -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Optional from rdflib import Graph from graph_db_interface.utils.iri import IRI +from graph_db_interface.utils.types import GraphNameLike if TYPE_CHECKING: from graph_db_interface import GraphDB @@ -9,7 +10,7 @@ def fetch_statements( self: "GraphDB", - graph_iri: Optional[Union[str, IRI]] = None, + graph_iri: Optional[GraphNameLike] = None, ) -> Optional[Graph]: """ Fetch statements from a named or the default graph. @@ -18,7 +19,7 @@ def fetch_statements( `rdflib.Graph`. When `graph_iri` is None, the default graph is fetched. Args: - graph_iri (Optional[Union[str, IRI]]): The named graph IRI to fetch; when + graph_iri (Optional[GraphNameLike]): The named graph IRI to fetch; when None, the default graph is fetched. Returns: @@ -61,7 +62,7 @@ def import_statements( self: "GraphDB", content: str, overwrite: Optional[bool] = False, - graph_iri: Optional[Union[str, IRI]] = None, + graph_iri: Optional[GraphNameLike] = None, content_type: Optional[str] = "application/x-turtle", ) -> bool: """ @@ -73,7 +74,7 @@ def import_statements( Args: content (str): RDF content to import. overwrite (Optional[bool]): Use PUT to overwrite existing content. Defaults to False. - graph_iri (Optional[Union[str, IRI]]): Target named graph IRI; default graph when None. + graph_iri (Optional[GraphNameLike]): Target named graph IRI; default graph when None. content_type (Optional[str]): MIME type of `content` (e.g., 'application/x-turtle'). Defaults to 'application/x-turtle'. @@ -117,7 +118,7 @@ def import_statements( def clear_graph( self: "GraphDB", - graph_iri: Optional[Union[str, IRI]] = None, + graph_iri: Optional[GraphNameLike] = None, ) -> bool: """ Clear a named graph or the default graph. @@ -126,7 +127,7 @@ def clear_graph( None, clears the default graph. Args: - graph_iri (Optional[Union[str, IRI]]): IRI of the named graph to clear; + graph_iri (Optional[GraphNameLike]): IRI of the named graph to clear; default graph when None. Returns: diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index cf07aca..0d3ddb6 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -1,10 +1,19 @@ # To be imported into ..graph_db.py GraphDB class from typing import List, Union, Any, Optional, Tuple, TYPE_CHECKING -from rdflib import Literal, BNode +from rdflib import BNode, Literal from graph_db_interface.utils import utils from graph_db_interface.utils.iri import IRI -from graph_db_interface.utils.utils import Triple, PartialTriple +from graph_db_interface.utils.types import ( + Subject, + Predicate, + PartialTripleLike, + SubjectLike, + PredicateLike, + ObjectLike, + GraphNameLike, + TriplesLike, +) from graph_db_interface.exceptions import InvalidInputError from graph_db_interface.sparql_query import SPARQLQuery @@ -15,29 +24,29 @@ def triples_get( self: "GraphDB", - triple: Optional[PartialTriple] = None, - sub: Optional[Union[str, IRI]] = None, - pred: Optional[Union[str, IRI]] = None, - obj: Optional[Union[str, IRI, Literal]] = None, + triple: Optional[PartialTripleLike] = None, + sub: Optional[SubjectLike] = None, + pred: Optional[PredicateLike] = None, + obj: Optional[ObjectLike] = None, include_explicit: Optional[bool] = True, include_implicit: Optional[bool] = True, - named_graph: Optional[Union[str, IRI]] = None, -) -> List[Tuple[IRI, IRI, Any]]: + named_graph: Optional[GraphNameLike] = None, +) -> List[Tuple[Subject, Predicate, Any]]: """ Retrieve triples matching any combination of subject, predicate, or object. Args: - triple (Optional[PartialTriple]): Combined (subject, predicate, object) filter tuple. Use this + triple (Optional[PartialTripleLike]): Combined (subject, predicate, object) filter tuple. Use this or individual `sub`/`pred`/`obj`. - sub (Optional[Union[str, IRI]]): Subject filter (IRI/shorthand/string). - pred (Optional[Union[str, IRI]]): Predicate filter (IRI/shorthand/string). - obj (Optional[Union[str, IRI, Literal]]): Object filter (IRI/shorthand/Literal/string). + sub (Optional[SubjectLike]): Subject filter (IRI/shorthand/string). + pred (Optional[PredicateLike]): Predicate filter (IRI/shorthand/string). + obj (Optional[ObjectLike]): Object filter (IRI/shorthand/Literal/string). include_explicit (Optional[bool]): Include explicit triples. Defaults to True. include_implicit (Optional[bool]): Include inferred triples. Defaults to True. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: - List[Tuple[IRI, IRI, Any]]: Matching triples as `(subject, predicate, object)`, where the + List[Tuple[Subject, Predicate, Any]]: Matching triples as `(subject, predicate, object)`, where the object is converted to an appropriate Python type when applicable. Raises: @@ -61,9 +70,9 @@ def triples_get( def _append_bind_and_filter( var: str, - value: Union[IRI, Literal], + value: Union[IRI, BNode, Literal], ) -> None: - if isinstance(value, IRI): + if isinstance(value, (IRI, BNode)): binds.append(f"BIND({value.n3()} AS {var})") elif isinstance(value, Literal): filter.append(f"FILTER(?o={value.n3()})") @@ -100,15 +109,15 @@ def _append_bind_and_filter( def any_triple_exists( self: "GraphDB", - triples: List[Triple], - named_graph: Optional[Union[str, IRI]] = None, + triples: TriplesLike, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Check if any of the given triples exist. Args: - triples (List[Triple]): Triples to check. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + triples (TriplesLike): Triples to check. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if at least one exists, False otherwise. @@ -135,15 +144,15 @@ def any_triple_exists( def all_triple_exists( self: "GraphDB", - triples: List[Triple], - named_graph: Optional[Union[str, IRI]] = None, + triples: TriplesLike, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Check if all of the given triples exist. Args: - triples (List[Triple]): Triples to check. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + triples (TriplesLike): Triples to check. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if all exist, False otherwise. @@ -182,17 +191,17 @@ def all_triple_exists( def triples_add( self: "GraphDB", - triples_to_add: List[Triple], + triples_to_add: TriplesLike, check_exist: Optional[bool] = True, - named_graph: Optional[Union[str, IRI]] = None, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Add multiple triples to the graph database. Args: - triples_to_add (List[Triple]): Triples to add. + triples_to_add (TriplesLike): Triples to add. check_exist (Optional[bool]): If True, abort when any triple already exists. Defaults to True. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if all triples were added, False otherwise. @@ -237,17 +246,17 @@ def triples_add( def triples_delete( self: "GraphDB", - triples_to_delete: List[Triple], + triples_to_delete: TriplesLike, check_exist: Optional[bool] = True, - named_graph: Optional[Union[str, IRI]] = None, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Delete multiple triples from the graph database. Args: - triples_to_delete (List[Triple]): Triples to delete. + triples_to_delete (TriplesLike): Triples to delete. check_exist (Optional[bool]): If True, abort when any triple does not exist. Defaults to True. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if all triples were deleted, False otherwise. @@ -292,19 +301,19 @@ def triples_delete( def triples_update( self: "GraphDB", - old_triples: List[Triple], - new_triples: List[Triple], + old_triples: TriplesLike, + new_triples: TriplesLike, check_exist: Optional[bool] = True, - named_graph: Optional[Union[str, IRI]] = None, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Update multiple RDF triples in the triplestore. Args: - old_triples (List[Triple]): Triples to be replaced. - new_triples (List[Triple]): Replacement triples (same length as `old_triples`). + old_triples (TriplesLike): Triples to be replaced. + new_triples (TriplesLike): Replacement triples (same length as `old_triples`). check_exist (Optional[bool]): If True, abort when any old triple does not exist. Defaults to True. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if the update was successful, False otherwise. diff --git a/graph_db_interface/queries/triple_single.py b/graph_db_interface/queries/triple_single.py index d8f02c8..dc036f3 100644 --- a/graph_db_interface/queries/triple_single.py +++ b/graph_db_interface/queries/triple_single.py @@ -1,11 +1,15 @@ # To be imported into ..graph_db.py GraphDB class -from typing import Optional, Union, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from graph_db_interface.utils import utils from graph_db_interface.utils.iri import IRI -from graph_db_interface.utils.utils import ( - Triple, - PartialTriple, +from graph_db_interface.utils.types import ( + PartialTripleLike, + SubjectLike, + PredicateLike, + ObjectLike, + GraphNameLike, + TripleLike, ) from graph_db_interface.exceptions import InvalidInputError @@ -17,15 +21,15 @@ def triple_exists( self: "GraphDB", - triple: Triple, - named_graph: Optional[Union[str, IRI]] = None, + triple: TripleLike, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Check whether a specific triple exists in the graph database. Args: - triple (Triple): The triple `(subject, predicate, object)` to check. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + triple (TripleLike): The triple `(subject, predicate, object)` to check. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if the triple exists, False otherwise. @@ -54,15 +58,15 @@ def triple_exists( def triple_add( self: "GraphDB", - triple: Triple, - named_graph: Optional[Union[str, IRI]] = None, + triple: TripleLike, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Add a triple to the graph database. Args: - triple (Triple): The triple `(subject, predicate, object)` to insert. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + triple (TripleLike): The triple `(subject, predicate, object)` to insert. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if the triple was inserted, False otherwise. @@ -90,9 +94,9 @@ def triple_add( def triple_delete( self: "GraphDB", - triple: Triple, + triple: TripleLike, check_exist: Optional[bool] = True, - named_graph: Optional[Union[str, IRI]] = None, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Delete a single triple. @@ -102,9 +106,9 @@ def triple_delete( present before attempting deletion. Args: - triple (Triple): The triple `(subject, predicate, object)` to delete. + triple (TripleLike): The triple `(subject, predicate, object)` to delete. check_exist (Optional[bool]): Whether to verify existence prior to deletion. Defaults to True. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if deletion succeeded (or triple absent when `check_exist=False`), False otherwise. @@ -136,13 +140,13 @@ def triple_delete( def triple_update( self: "GraphDB", - old_triple: Triple, - new_triple: Optional[PartialTriple] = None, - new_sub: Optional[Union[str, IRI]] = None, - new_pred: Optional[Union[str, IRI]] = None, - new_obj: Optional[Union[str, IRI]] = None, + old_triple: TripleLike, + new_triple: Optional[PartialTripleLike] = None, + new_sub: Optional[SubjectLike] = None, + new_pred: Optional[PredicateLike] = None, + new_obj: Optional[ObjectLike] = None, check_exist: Optional[bool] = True, - named_graph: Optional[Union[str, IRI]] = None, + named_graph: Optional[GraphNameLike] = None, ) -> bool: """ Update a triple by replacing any of its parts. @@ -151,14 +155,14 @@ def triple_update( with a new triple built from provided parts. Args: - old_triple (Triple): Existing triple to update. - new_triple (Optional[PartialTriple]): Replacement values (subject/predicate/object). Use this + old_triple (TripleLike): Existing triple to update. + new_triple (Optional[PartialTripleLike]): Replacement values (subject/predicate/object). Use this or `new_sub`/`new_pred`/`new_obj`. - new_sub (Optional[Union[str, IRI]]): Replacement subject. - new_pred (Optional[Union[str, IRI]]): Replacement predicate. - new_obj (Optional[Union[str, IRI]]): Replacement object. + new_sub (Optional[SubjectLike]): Replacement subject. + new_pred (Optional[PredicateLike]): Replacement predicate. + new_obj (Optional[ObjectLike]): Replacement object. check_exist (Optional[bool]): If True, verify that `old_triple` exists before updating. Defaults to True. - named_graph (Optional[Union[str, IRI]]): Override the client's default named graph. + named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: bool: True if the update succeeded, False otherwise. diff --git a/graph_db_interface/sparql_query.py b/graph_db_interface/sparql_query.py index b4325d7..1be2d46 100644 --- a/graph_db_interface/sparql_query.py +++ b/graph_db_interface/sparql_query.py @@ -1,8 +1,8 @@ from enum import Enum -from typing import List, Optional, Union +from typing import List, Optional from graph_db_interface.utils import utils -from graph_db_interface.utils.utils import Triple from graph_db_interface.utils.iri import IRI +from graph_db_interface.utils.types import TriplesLike, GraphNameLike class SPARQLQueryType(Enum): @@ -30,7 +30,7 @@ class SPARQLQuery: def __init__( self, - named_graph: Optional[Union[str, IRI]] = None, + named_graph: Optional[GraphNameLike] = None, include_explicit: Optional[bool] = True, include_implicit: Optional[bool] = True, ): @@ -124,7 +124,7 @@ def add_ask_block( @classmethod def insert_data( cls, - triples: List[Triple], + triples: TriplesLike, **kwargs, ) -> "SPARQLQuery": """ @@ -137,13 +137,13 @@ def insert_data( def add_insert_data_block( self, - triples: List[Triple], + triples: TriplesLike, ) -> None: """ Add an INSERT DATA block comprised of triples. Args: - triples (List[Triple]): Triples to insert. + triples (TriplesLike): Triples to insert. """ block_parts = [] data_combined = "\n".join( @@ -162,7 +162,7 @@ def add_insert_data_block( @classmethod def insert_exists( cls, - triples: List[Triple], + triples: TriplesLike, **kwargs, ) -> "SPARQLQuery": """ @@ -175,13 +175,13 @@ def insert_exists( def add_insert_exists_block( self, - triples: List[Triple], + triples: TriplesLike, ) -> None: """ Add an INSERT ... WHERE NOT EXISTS block. Args: - triples (List[Triple]): Triples to insert when they do not already exist. + triples (TriplesLike): Triples to insert when they do not already exist. """ block_parts = [] data_combined = "\n".join( @@ -205,7 +205,7 @@ def add_insert_exists_block( @classmethod def delete_data( cls, - triples: List[Triple], + triples: TriplesLike, **kwargs, ) -> "SPARQLQuery": """ @@ -218,13 +218,13 @@ def delete_data( def add_delete_data_block( self, - triples: List[Triple], + triples: TriplesLike, ) -> None: """ Add a DELETE DATA block comprised of triples. Args: - triples (List[Triple]): Triples to delete. + triples (TriplesLike): Triples to delete. """ block_parts = [] data_combined = "\n".join( @@ -243,8 +243,8 @@ def add_delete_data_block( @classmethod def delete_insert_data( cls, - delete_triples: List[Triple], - insert_triples: List[Triple], + delete_triples: TriplesLike, + insert_triples: TriplesLike, where_clauses: List[str], **kwargs, ) -> "SPARQLQuery": @@ -262,16 +262,16 @@ def delete_insert_data( def add_delete_insert_data_block( self, - delete_triples: List[Triple], - insert_triples: List[Triple], + delete_triples: TriplesLike, + insert_triples: TriplesLike, where_clauses: List[str], ) -> None: """ Add a combined DELETE/INSERT block with WHERE. Args: - delete_triples (List[Triple]): Triples to delete. - insert_triples (List[Triple]): Triples to insert. + delete_triples (TriplesLike): Triples to delete. + insert_triples (TriplesLike): Triples to insert. where_clauses (List[str]): WHERE patterns selecting the triples to update. """ block_parts = [] diff --git a/graph_db_interface/utils/types.py b/graph_db_interface/utils/types.py new file mode 100644 index 0000000..944a538 --- /dev/null +++ b/graph_db_interface/utils/types.py @@ -0,0 +1,41 @@ +from typing import Any, TypeAlias, Union, Set, List, Optional, Tuple +from rdflib import Literal, BNode + +from graph_db_interface.utils.iri import IRI + +GraphName: TypeAlias = IRI + +Subject: TypeAlias = Union[IRI, BNode] +Predicate: TypeAlias = IRI +Object: TypeAlias = Union[IRI, BNode, Literal] + +Triple: TypeAlias = Tuple[Subject, Predicate, Object] +PartialTriple: TypeAlias = Tuple[ + Optional[Subject], Optional[Predicate], Optional[Object] +] + +Triples: TypeAlias = Union[Set[Triple], List[Triple], Tuple[Triple]] +PartialTriples: TypeAlias = Union[ + Set[PartialTriple], List[PartialTriple], Tuple[PartialTriple] +] + + +IRILike: TypeAlias = Union[str, IRI] +BNodeLike: TypeAlias = Union[str, BNode] +LiteralLike: TypeAlias = Union[Any, Literal] + +GraphNameLike: TypeAlias = IRILike + +SubjectLike: TypeAlias = Union[IRILike, BNodeLike] +PredicateLike: TypeAlias = IRILike +ObjectLike: TypeAlias = Union[IRILike, BNodeLike, LiteralLike] + +TripleLike: TypeAlias = Tuple[SubjectLike, PredicateLike, ObjectLike] +PartialTripleLike: TypeAlias = Tuple[ + Optional[SubjectLike], Optional[PredicateLike], Optional[ObjectLike] +] + +TriplesLike: TypeAlias = Union[Set[TripleLike], List[TripleLike], Tuple[TripleLike]] +PartialTriplesLike: TypeAlias = Union[ + Set[PartialTripleLike], List[PartialTripleLike], Tuple[PartialTripleLike] +] diff --git a/graph_db_interface/utils/utils.py b/graph_db_interface/utils/utils.py index 1e2c8b6..4a87bc8 100644 --- a/graph_db_interface/utils/utils.py +++ b/graph_db_interface/utils/utils.py @@ -1,5 +1,6 @@ import logging -from typing import TypeAlias, Union, Optional, Any, Tuple +import re +from typing import Union, Optional, Any from rdflib import Literal, XSD, Dataset, BNode, URIRef from rdflib.plugins.sparql.processor import prepareQuery from graph_db_interface.exceptions import ( @@ -7,27 +8,31 @@ InvalidQueryError, ) from graph_db_interface.utils.iri import IRI -from graph_db_interface.utils.typemap import XSDToPythonMapper, XSDToPythonTypes +from graph_db_interface.utils.types import ( + BNodeLike, + IRILike, + Triple, + PartialTriple, + TripleLike, + PartialTripleLike, + Subject, + Predicate, + Object, + SubjectLike, + PredicateLike, + ObjectLike, +) +from graph_db_interface.utils.xsd_typemap import XSDToPythonMapper, XSDToPythonTypes LOGGER = logging.getLogger(__name__) -Triple: TypeAlias = Tuple[ - Union[str, IRI], - Union[str, IRI], - Union[str, IRI, Literal], -] - -PartialTriple: TypeAlias = Tuple[ - Optional[Union[str, IRI]], - Optional[Union[str, IRI]], - Optional[Union[str, IRI, Literal]], -] +BNODE_PATTERN = re.compile(r"^_:[a-zA-Z][a-zA-Z0-9_\-]*$") def sanitize_triple( - triple: Triple, + triple: Union[TripleLike, PartialTripleLike], allow_partial: Optional[bool] = False, -) -> Triple: +) -> Union[Triple, PartialTriple]: """ Validates and converts the components of a triple to their appropriate types. @@ -42,12 +47,11 @@ def sanitize_triple( - If the object is of any other type, it will be converted to a Literal. Args: - triple (Triple): The triple to validate and convert. + triple (Union[TripleLike, PartialTripleLike]): The triple to validate and convert. allow_partial (Optional[bool]): Whether to allow partial triples. Defaults to False. Returns: - Triple: The validated and converted triple. - + Union[Triple, PartialTriple]: The validated and converted triple. Raises: InvalidInputError: If the triple does not have three or too many None entries. TypeError: If subject or predicate are not of type str, URIRef, or IRI. @@ -69,44 +73,65 @@ def sanitize_triple( sub, pred, obj = triple - # Sub must be IRI - if sub is not None and not isinstance(sub, IRI): - sub = IRI(sub) - - # Pred must be IRI - if pred is not None and not isinstance(pred, IRI): - pred = IRI(pred) - - # Pred must be IRI or Literal - # If str, try IRI first, then Literal. Otherwise, Literal. - if obj is not None and not isinstance(obj, (IRI, Literal)): - if isinstance(obj, str): - try: - obj = IRI(str(obj)) - except Exception as e: - error_message = ( - "Object is of type str but cannot be converted to IRI. If object " - + f"is a , explicitly convert before passing: {obj} ({e})" - ) - LOGGER.error(error_message) - raise type(e)(error_message) from e - else: - obj = Literal(obj) + if sub is not None and not isinstance(sub, Subject): + sub = _to_subject(sub) + if pred is not None and not isinstance(pred, Predicate): + pred = _to_predicate(pred) + if obj is not None and not isinstance(obj, Object): + obj = _to_object(obj) + return sub, pred, obj +def _to_iri_or_bnode(value: Union[IRILike, BNodeLike]) -> Union[IRI, BNode]: + """Try converting a IRILike or BNodeLike.""" + if isinstance(value, Union[IRI, BNode]): + return value + + if BNODE_PATTERN.match(str(value)): + return BNode(value[2:]) # Remove '_:' prefix for BNode id + + return IRI(value) + + +def _to_subject(sub: SubjectLike) -> Subject: + """Convert a SubjectLike to a Subject.""" + return _to_iri_or_bnode(sub) + + +def _to_predicate(pred: PredicateLike) -> Predicate: + """Convert a PredicateLike to a Predicate.""" + return IRI(pred) + + +def _to_object(obj: ObjectLike) -> Object: + """Convert an ObjectLike to an Object.""" + if isinstance(obj, Object): + return obj + + if isinstance(obj, str): + try: + return _to_iri_or_bnode(str(obj)) + except Exception as e: + error_message = f"Object is of type str but cannot be converted to IRI or BNode. If object is a , explicitly convert before passing: {obj} ({e})" + LOGGER.error(error_message) + raise type(e)(error_message) from e + else: + return Literal(obj) + + def triple_to_string( - triple: Triple, + triple: TripleLike, line_end: Optional[str] = None, ) -> str: """Convert a triple to its string representation suitable for SPARQL queries. Args: - triple (Triple): The triple to convert. + triple (TripleLike): The triple to convert. Returns: str: The string representation of the triple. """ - sub, pred, obj = triple + sub, pred, obj = sanitize_triple(triple) return f"{sub.n3()} {pred.n3()} {obj.n3()}" + (f" {line_end}" if line_end else "") diff --git a/tests/test_utils.py b/tests/test_utils.py index a61382c..781c10f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,11 +6,11 @@ ) from graph_db_interface.utils.iri import IRI import pytest -from rdflib import Literal +from rdflib import BNode, Literal import datetime -def test_sanitize_triple(): +def test_sanitize_triple_iris(): triple_three_iri = ( IRI("http://example.org#subject"), IRI("http://example.org#predicate"), @@ -25,10 +25,41 @@ def test_sanitize_triple(): "http://example.org#predicate", "http://example.org#object", ) - # Must convert first two str to IRI, last to Literal + # Must convert all three to IRI sanitized_triple = utils.sanitize_triple(triple) assert sanitized_triple == triple_three_iri + +def test_sanitize_triple_bnodes(): + triple_bnode = ( + BNode("genid-123"), + IRI("http://example.org#predicate"), + BNode("genid-456"), + ) + # Must not modify + sanitized_triple = utils.sanitize_triple(triple_bnode) + assert sanitized_triple == triple_bnode + + triple_bnode_string = ( + "_:genid-123", + "http://example.org#predicate", + "_:genid-456", + ) + # Must convert to BNode, IRI + sanitized_triple = utils.sanitize_triple(triple_bnode_string) + assert sanitized_triple == triple_bnode + + triple = ( + IRI("http://example.org#subject"), + BNode("genid-123"), + IRI("http://example.org#object"), + ) + # Predicate is BNode, is illegal + with pytest.raises(InvalidIRIError): + utils.sanitize_triple(triple) + + +def test_sanitize_triple_literals(): triple_iri_literal_string = ( IRI("http://example.org#subject"), IRI("http://example.org#predicate"), @@ -75,19 +106,20 @@ def test_sanitize_triple(): utils.sanitize_triple(triple) triple = ( - IRI("http://example.org#subject"), + Literal("http://example.org#subject"), IRI("http://example.org#predicate"), + IRI("http://example.org#object"), ) - # only 2 elements - with pytest.raises(InvalidInputError): + # Subject is Literal, is illegal + with pytest.raises(TypeError): utils.sanitize_triple(triple) triple = ( - Literal("http://example.org#subject"), + IRI("http://example.org#subject"), Literal("http://example.org#predicate"), IRI("http://example.org#object"), ) - # first two elements are Literals, must be IRIs + # Predicate is Literal, is illegal with pytest.raises(TypeError): utils.sanitize_triple(triple) From 1fbd5016a1b0cd9b9f8c8be343853ef538b8e54f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Fri, 9 Jan 2026 20:20:13 +0100 Subject: [PATCH 26/36] Rename typemap to xsd_typemap to distinguish it from python types --- graph_db_interface/__init__.py | 2 +- graph_db_interface/utils/{typemap.py => xsd_typemap.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename graph_db_interface/utils/{typemap.py => xsd_typemap.py} (100%) diff --git a/graph_db_interface/__init__.py b/graph_db_interface/__init__.py index 4b2fec8..97dffdb 100644 --- a/graph_db_interface/__init__.py +++ b/graph_db_interface/__init__.py @@ -6,7 +6,7 @@ from .utils.processing import process_bindings_select from .utils.pretty_print import format_result from .kafka.kafka_manager import KafkaManager -from .utils.typemap import XSDToPythonTypes, XSDToPythonMapper +from .utils.xsd_typemap import XSDToPythonTypes, XSDToPythonMapper __all__ = [ "GraphDB", diff --git a/graph_db_interface/utils/typemap.py b/graph_db_interface/utils/xsd_typemap.py similarity index 100% rename from graph_db_interface/utils/typemap.py rename to graph_db_interface/utils/xsd_typemap.py From 32fa3ede3444d9f8d0ae8536a405483091b01f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 12 Jan 2026 17:08:02 +0100 Subject: [PATCH 27/36] Modify new_iri to new schema IRI -> IRI --- .../queries/ontology_helpers.py | 25 +++++++----- tests/test_graph_manipulation.py | 40 +++++++++++-------- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index bb4fcef..5f40b1a 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -79,30 +79,33 @@ def iri_exists( def new_iri( self: "GraphDB", base: IRILike, - schema: Optional[Callable[[], str]] = lambda: str(uuid.uuid4()), + schema: Optional[Callable[[IRI], IRI]] = lambda base: ( + f"{base}-{uuid.uuid4()}" if base.fragment else f"{base}#instance-{uuid.uuid4()}" + ), + test_schema: bool = True, ) -> IRI: """ Generate a new unique IRI within the graph database's namespace. Args: base (IRILike): The base IRI or namespace for the new IRI. - schema (Optional[Callable[[], str]]): A callable that generates the unique part - of the IRI. Defaults to a UUID4 string generator. + schema (Optional[Callable[[IRI], IRI]]): A callable that generates the new IRI. + Takes the base IRI as an argument. Defaults to a `{onto}#{fragment}-{UUID4}`. + If fragment of base is empty, uses `{onto}#instance-{UUID4}`. Returns: IRI: A new unique IRI. """ - if schema() == schema(): - raise ValueError("Schema function must produce different values on each call") + if base is None: + raise InvalidInputError("Base IRI must be provided for new IRI generation") - base = base.rstrip("#") - if "#" in base: - base, fragment_start = base.split("#", 1) - else: - fragment_start = "" + base = IRI(base) + + if test_schema and schema(base) == schema(base): + raise ValueError("Schema function must produce different values on each call") def new() -> str: - return IRI(value=fragment_start + schema(), base=base) + return IRI(schema(base)) iri = new() diff --git a/tests/test_graph_manipulation.py b/tests/test_graph_manipulation.py index 23e6470..3386a26 100644 --- a/tests/test_graph_manipulation.py +++ b/tests/test_graph_manipulation.py @@ -1,4 +1,4 @@ -import random +import itertools import pytest from rdflib import Literal, XSD from graph_db_interface import GraphDB @@ -604,56 +604,62 @@ def test_convenience_functions(db: GraphDB, named_graph: str): def test_iri_generation(db: GraphDB, named_graph: str): - valid_schema = lambda: str(random.randint(0, 1000000)) - invalid_schema = lambda: "fixed" + counter = itertools.count() + valid_iri_schema = lambda base: f"{base}#{counter.__next__()}" + invalid_iri_schema = lambda base: f"{base}#fixed" + valid_genid_schema = lambda: f"blank-{counter.__next__()}" + invalid_genid_schema = lambda: "fixed-blank" + + base_no_fragment = "http://example.org" + base_with_fragment = "http://example.org#ClassName" # Generate a new IRI iri1 = db.new_iri( - base="http://example.org", + base=base_no_fragment, ) assert isinstance(iri1, IRI) assert iri1.onto == "http://example.org" # Ensure uniqueness iri2 = db.new_iri( - base="http://example.org", + base=base_no_fragment, ) assert iri1 != iri2 # Generate with fragment base iri3 = db.new_iri( - base="http://example.org#ClassName_", + base=base_with_fragment, ) assert isinstance(iri3, IRI) assert iri3.onto == "http://example.org" - assert iri3.fragment.startswith("ClassName_") + assert iri3.fragment.startswith("ClassName-") # Ensure uniqueness iri4 = db.new_iri( - base="http://example.org#ClassName_", + base=base_with_fragment, ) assert iri3 != iri4 # Generate with schema iri5 = db.new_iri( - base="http://example.org", - schema=valid_schema, + base=base_no_fragment, + schema=valid_iri_schema, ) assert isinstance(iri5, IRI) assert iri5.onto == "http://example.org" assert iri1 != iri5 iri6 = db.new_iri( - base="http://example.org", - schema=valid_schema, + base=base_no_fragment, + schema=valid_iri_schema, ) assert iri5 != iri6 # Invalid schema that does not produce unique IRIs with pytest.raises(ValueError): db.new_iri( - base="http://example.org", - schema=invalid_schema, + base=base_no_fragment, + schema=invalid_iri_schema, ) # Generate blank node IDs @@ -665,18 +671,18 @@ def test_iri_generation(db: GraphDB, named_graph: str): # Generate with schema genid3 = db.new_blank_id( - schema=valid_schema, + schema=valid_genid_schema, ) assert isinstance(genid3, str) assert genid1 != genid3 genid4 = db.new_blank_id( - schema=valid_schema, + schema=valid_genid_schema, ) assert genid3 != genid4 # Invalid schema that does not produce unique blank IDs with pytest.raises(ValueError): db.new_blank_id( - schema=invalid_schema, + schema=invalid_genid_schema, ) From faf79e33d7136fdd5d9d7853c3f4bac2cfea1646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 12 Jan 2026 22:38:45 +0100 Subject: [PATCH 28/36] Fix bug in new_iri where passing `schema=None` breaks the method --- graph_db_interface/queries/ontology_helpers.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index 5f40b1a..c4a6e2f 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -79,9 +79,7 @@ def iri_exists( def new_iri( self: "GraphDB", base: IRILike, - schema: Optional[Callable[[IRI], IRI]] = lambda base: ( - f"{base}-{uuid.uuid4()}" if base.fragment else f"{base}#instance-{uuid.uuid4()}" - ), + schema: Optional[Callable[[IRI], IRI]] = None, test_schema: bool = True, ) -> IRI: """ @@ -101,7 +99,13 @@ def new_iri( base = IRI(base) - if test_schema and schema(base) == schema(base): + if schema is None: + schema = lambda base: ( + f"{base}-{uuid.uuid4()}" + if base.fragment + else f"{base}#instance-{uuid.uuid4()}" + ) + elif test_schema and schema(base) == schema(base): raise ValueError("Schema function must produce different values on each call") def new() -> str: From 2a9d85910c5462a75aadf654746142f6f2754721 Mon Sep 17 00:00:00 2001 From: Etienne Hoffmann Date: Mon, 19 Jan 2026 16:15:25 +0100 Subject: [PATCH 29/36] Enhance IRI class with lined property for Punycode-style encoding and from_lined method for decoding --- graph_db_interface/utils/iri.py | 45 ++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/graph_db_interface/utils/iri.py b/graph_db_interface/utils/iri.py index 77b4acb..6e2a482 100644 --- a/graph_db_interface/utils/iri.py +++ b/graph_db_interface/utils/iri.py @@ -85,12 +85,49 @@ def short(self) -> str: @property def lined(self) -> str: """ - Return a version of the iri with symbols ".:/#" replaced by "_" + Convert IRI to valid Python identifier using Punycode-style markers. - Returns: - str: Cleaned up name + Encoding: _ → __, : → _c_, / → _s_, . → _d_, # → _h_ + + Human-readable and fully reversible. Markers mnemonically represent their chars: + _c_ = colon, _s_ = slash, _d_ = dot, _h_ = hash. + + Example: https://example.com#Property → https_c_example_d_com_h_Property + """ + result = [] + for char in str(self): + if char == "_": + result.append("__") + elif char == ":": + result.append("_c_") + elif char == "/": + result.append("_s_") + elif char == ".": + result.append("_d_") + elif char == "#": + result.append("_h_") + else: + result.append(char) + return "".join(result) + + @classmethod + def from_lined(cls, lined: str) -> IRI: """ - return "_".join(filter(None, re.split("\.|:|/|#", self))) + Decode a lined identifier back to full IRI. + + Reverses the encoding applied by `lined` property. + Decode order: _c_→:, _s_→/, _d_→., _h_→#, then __→_. + + Example: https_c_example_d_com_h_Property → https://example.com#Property + """ + iri_str = ( + lined.replace("_c_", ":") + .replace("_s_", "/") + .replace("_d_", ".") + .replace("_h_", "#") + .replace("__", "_") + ) + return cls(iri_str) @property def onto(self) -> str: From 42abdd783d69503b036151ddb0da7651559ffa15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Mon, 19 Jan 2026 19:21:09 +0100 Subject: [PATCH 30/36] Add test to IRI.lined and IRI.from_lined --- graph_db_interface/utils/iri.py | 45 +++++++++++++++------------------ tests/test_iri.py | 25 ++++++++++++++++++ 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/graph_db_interface/utils/iri.py b/graph_db_interface/utils/iri.py index 6e2a482..3d06110 100644 --- a/graph_db_interface/utils/iri.py +++ b/graph_db_interface/utils/iri.py @@ -44,6 +44,17 @@ class IRI(URIRef): # Valid schemes for full IRIs, end with '://' SCHEMES = {"http://", "https://"} + # Patterns for lined encoding/decoding. Underscore must be first. + LINED_PATTERN = [ + ("_", "__"), + (":", "_c_"), + ("/", "_s_"), + (".", "_d_"), + ("#", "_h_"), + ] + # Patterns for lined encoding/decoding. Underscore must be last. + LINED_PATTERN_INV = [(b, a) for a, b in reversed(LINED_PATTERN)] + def __new__( cls, value: str, @@ -92,23 +103,12 @@ def lined(self) -> str: Human-readable and fully reversible. Markers mnemonically represent their chars: _c_ = colon, _s_ = slash, _d_ = dot, _h_ = hash. - Example: https://example.com#Property → https_c_example_d_com_h_Property + Example: https://example.com#Property → https_c__s__s_example_d_com_h_Property """ - result = [] - for char in str(self): - if char == "_": - result.append("__") - elif char == ":": - result.append("_c_") - elif char == "/": - result.append("_s_") - elif char == ".": - result.append("_d_") - elif char == "#": - result.append("_h_") - else: - result.append(char) - return "".join(result) + lined = str(self) + for a, b in self.LINED_PATTERN: + lined = lined.replace(a, b) + return lined @classmethod def from_lined(cls, lined: str) -> IRI: @@ -118,16 +118,11 @@ def from_lined(cls, lined: str) -> IRI: Reverses the encoding applied by `lined` property. Decode order: _c_→:, _s_→/, _d_→., _h_→#, then __→_. - Example: https_c_example_d_com_h_Property → https://example.com#Property + Example: https_c__s__s_example_d_com_h_Property → https://example.com#Property """ - iri_str = ( - lined.replace("_c_", ":") - .replace("_s_", "/") - .replace("_d_", ".") - .replace("_h_", "#") - .replace("__", "_") - ) - return cls(iri_str) + for a, b in cls.LINED_PATTERN_INV: + lined = lined.replace(a, b) + return cls(lined) @property def onto(self) -> str: diff --git a/tests/test_iri.py b/tests/test_iri.py index 23396b0..87137fb 100644 --- a/tests/test_iri.py +++ b/tests/test_iri.py @@ -308,3 +308,28 @@ def test_short(): assert iri1.short == "owl:Class" assert iri3.short == "" + + +def test_lined(): + iri1 = IRI("http://www.w3.org/2002/07/owl#Class") + iri2 = IRI("https://example.com/path/to/resource#Property") + iri3 = IRI("http://example.com#A_B_C") + iri4 = IRI("https://sub.domain.co.uk/path.to/resource_name#Property-1") + + assert iri1.lined == "http_c__s__s_www_d_w3_d_org_s_2002_s_07_s_owl_h_Class" + assert iri2.lined == "https_c__s__s_example_d_com_s_path_s_to_s_resource_h_Property" + assert iri3.lined == "http_c__s__s_example_d_com_h_A__B__C" + assert ( + iri4.lined + == "https_c__s__s_sub_d_domain_d_co_d_uk_s_path_d_to_s_resource__name_h_Property-1" + ) + + iri1_reconstructed = IRI.from_lined(iri1.lined) + iri2_reconstructed = IRI.from_lined(iri2.lined) + iri3_reconstructed = IRI.from_lined(iri3.lined) + iri4_reconstructed = IRI.from_lined(iri4.lined) + + assert iri1_reconstructed == iri1 + assert iri2_reconstructed == iri2 + assert iri3_reconstructed == iri3 + assert iri4_reconstructed == iri4 From 1b0ed1a31145d13fb86218f9527769379eaff042 Mon Sep 17 00:00:00 2001 From: Etienne Hoffmann Date: Tue, 20 Jan 2026 16:17:24 +0100 Subject: [PATCH 31/36] Improve lined property documentation with detailed encoding explanation and examples --- graph_db_interface/utils/iri.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/graph_db_interface/utils/iri.py b/graph_db_interface/utils/iri.py index 3d06110..a2e7b53 100644 --- a/graph_db_interface/utils/iri.py +++ b/graph_db_interface/utils/iri.py @@ -96,14 +96,24 @@ def short(self) -> str: @property def lined(self) -> str: """ - Convert IRI to valid Python identifier using Punycode-style markers. + Convert IRI to valid Python identifier using reversible marker encoding. - Encoding: _ → __, : → _c_, / → _s_, . → _d_, # → _h_ + Character encoding: + - _ → __ (underscore escaped to preserve marker distinction) + - : → _c_ (colon) + - / → _s_ (slash) + - . → _d_ (dot) + - # → _h_ (hash) - Human-readable and fully reversible. Markers mnemonically represent their chars: - _c_ = colon, _s_ = slash, _d_ = dot, _h_ = hash. + This is a simple, human-readable alternative to Punycode—optimized for + debugging and direct IRI recognition in code, not for compression. - Example: https://example.com#Property → https_c__s__s_example_d_com_h_Property + Markers use mnemonic single letters (_c_=colon, _s_=slash, etc.) for + easy visual decoding. Order of replacement matters for reversibility. + + Example: + https://www.sfb1574.kit.edu/ontologies/TransferUnit#hasConveyorBelt + → https_c_www_d_sfb1574_d_kit_d_edu_s_ontologies_s_TransferUnit_h_hasConveyorBelt """ lined = str(self) for a, b in self.LINED_PATTERN: From 853feffe11ec0ee7a3c90a3434518d65a19446fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Fri, 30 Jan 2026 12:46:11 +0100 Subject: [PATCH 32/36] WIP: Fix any_triple_exists --- .../queries/ontology_helpers.py | 14 ++--- graph_db_interface/queries/triple_multi.py | 45 ++++++++++---- graph_db_interface/utils/utils.py | 59 ++++++++++++++++++- 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index c4a6e2f..78a2e83 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -1,7 +1,7 @@ # To be imported into ..graph_db.py GraphDB class import uuid -from typing import List, Optional, Union, Callable, TYPE_CHECKING +from typing import List, Optional, Set, Union, Callable, TYPE_CHECKING from graph_db_interface.utils import utils from graph_db_interface.utils.iri import IRI from graph_db_interface.utils.types import IRILike, GraphNameLike @@ -200,7 +200,7 @@ def owl_get_classes_of_individual( include_explicit: Optional[bool] = True, include_implicit: Optional[bool] = False, named_graph: Optional[GraphNameLike] = None, -) -> List[Union[IRI, str]]: +) -> Set[Union[IRI, str]]: """ Get the OWL classes associated with a given individual. @@ -217,7 +217,7 @@ def owl_get_classes_of_individual( named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: - List[Union[IRI, str]]: Class IRIs, or local names if `local_name=True`. + Set[Union[IRI, str]]: Class IRIs, or local names if `local_name=True`. """ instance_iri = IRI(instance_iri) named_graph = IRI(named_graph) if named_graph is not None else self.named_graph @@ -253,14 +253,14 @@ def owl_get_classes_of_individual( results = self.query(query=query, convert_bindings=True) if results is None: - return [] + return set() if local_name: - classes = [ + classes = { utils.get_local_name(result["class"]) for result in results["results"]["bindings"] - ] + } return classes - classes = [result["class"] for result in results["results"]["bindings"]] + classes = {result["class"] for result in results["results"]["bindings"]} return classes diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index 0d3ddb6..6956f29 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -1,6 +1,6 @@ # To be imported into ..graph_db.py GraphDB class -from typing import List, Union, Any, Optional, Tuple, TYPE_CHECKING +from typing import Dict, List, Union, Any, Optional, Tuple, Set, TYPE_CHECKING from rdflib import BNode, Literal from graph_db_interface.utils import utils from graph_db_interface.utils.iri import IRI @@ -13,6 +13,7 @@ ObjectLike, GraphNameLike, TriplesLike, + Triple, ) from graph_db_interface.exceptions import InvalidInputError @@ -127,17 +128,38 @@ def any_triple_exists( if not triples: raise InvalidInputError(f"Cannot check existence of empty triple list.") - for triple in triples: - triple = utils.sanitize_triple(triple) + validated_triples = {utils.sanitize_triple(triple) for triple in triples} - if self.triple_exists(triple, named_graph=named_graph): - self.logger.debug( - f"At least this triple exists: ({utils.triple_to_string(triple)}), named_graph: {named_graph or "default"}, repository: {self._repository}" - ) - return True + triple_groups = utils.group_triples_by_bnode(validated_triples) + + # Build UNION patterns for ASK query + union_patterns = [] + for group in triple_groups: + group_pattern = " .\n ".join( + utils.triple_to_string(triple, "") for triple in group + ) + union_patterns.append(f"{{\n {group_pattern} .\n }}") + + where_clause = "\n UNION\n ".join(union_patterns) + + query = SPARQLQuery.ask( + where_clauses=[where_clause], + named_graph=named_graph, + ) + ask_result = self.query(query=query, update=False) + if ask_result is None: + raise InvalidInputError( + f"Could not query 'any_triple_exists' for triples, named_graph: {named_graph or 'default'}, repository: {self._repository}" + ) + + if ask_result["boolean"] is True: + self.logger.debug( + f"At least one of the triples exists, named_graph: {named_graph or 'default'}, repository: {self._repository}" + ) + return True self.logger.debug( - f"None of the triples exists: ({triples}), named_graph: {named_graph or "default"}, repository: {self._repository}" + f"None of the triples exists, named_graph: {named_graph or 'default'}, repository: {self._repository}" ) return False @@ -157,6 +179,7 @@ def all_triple_exists( Returns: bool: True if all exist, False otherwise. """ + self.logger.setLevel(10) named_graph = IRI(named_graph) if named_graph is not None else self.named_graph if not triples: @@ -323,8 +346,8 @@ def triples_update( if not old_triples and not new_triples: return True - if len(old_triples) != len(new_triples): - raise InvalidInputError("Old and new triples lists must have the same length.") + # if len(old_triples) != len(new_triples): + # raise InvalidInputError("Old and new triples lists must have the same length.") validated_old_triples = [utils.sanitize_triple(triple) for triple in old_triples] validated_new_triples = [utils.sanitize_triple(triple) for triple in new_triples] diff --git a/graph_db_interface/utils/utils.py b/graph_db_interface/utils/utils.py index 4a87bc8..b1229a4 100644 --- a/graph_db_interface/utils/utils.py +++ b/graph_db_interface/utils/utils.py @@ -1,6 +1,6 @@ import logging import re -from typing import Union, Optional, Any +from typing import Set, Tuple, Union, Optional, Any, Dict from rdflib import Literal, XSD, Dataset, BNode, URIRef from rdflib.plugins.sparql.processor import prepareQuery from graph_db_interface.exceptions import ( @@ -342,3 +342,60 @@ def encapsulate_named_graph( {content} }}""" return content + + +def group_triples_by_bnode(triples: Set[Triple]) -> Tuple[Set[Triple]]: + """ + Groups triples sharing blank nodes into connected components to ensure + blank node semantics are preserved within query scope. + + Args: + triples (Set[Triple]): Triples to check. + + Retruns: + Tuple[Set[Triple]]: A tuple of sets of related triples + + Example: + ASK { + {

. } # Independent triple + UNION + { _:b1 . + _:b1 . } # Connected via blank node + UNION + { . } # Another independent triple + } + """ + triple_groups: Dict[BNode, Set[Triple]] = {} + + for triple in triples: + # Extract blank nodes in the triple + bnodes = set() + for elem in triple: + if isinstance(elem, BNode): + bnodes.add(elem) + + if not bnodes: + # No blank nodes - independent triple + # Use triple itself as hashable key + triple_groups[triple] = {triple} + continue + + containing_sets = [ + triple_groups[bnode] for bnode in bnodes if bnode in triple_groups + ] + if not containing_sets: + # Create new group + containing_set = set() + else: + # Existing groups - merge if multiple + containing_set = containing_sets[0] + for set_to_merge in containing_sets[1:]: + containing_set |= set_to_merge + + # Add triple to the containing group + containing_set.add(triple) + # Update bnode to group mapping - overwrite to merged group if needed + for bnode in bnodes: + triple_groups[bnode] = containing_set + + return tuple(triple_groups.values()) From 85c4886beec71d1c89461700bce6ca3d929b3839 Mon Sep 17 00:00:00 2001 From: Etienne Hoffmann Date: Tue, 10 Feb 2026 10:41:15 +0100 Subject: [PATCH 33/36] Refactor triples_update to enhance SPARQL query generation with improved pattern handling and blank node management --- graph_db_interface/queries/triple_multi.py | 78 +++++++++++++++++++--- 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index 6956f29..1e4d28c 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -61,6 +61,8 @@ def triples_get( ) named_graph = IRI(named_graph) if named_graph is not None else self.named_graph + if named_graph is not None and not hasattr(named_graph, "n3"): + named_graph = IRI(named_graph) sub, pred, obj = utils.sanitize_triple( triple or (sub, pred, obj), allow_partial=True @@ -360,16 +362,74 @@ def triples_update( ) return False - old_triple_strings = [ - utils.triple_to_string(triple, ".") for triple in validated_old_triples - ] - - query = SPARQLQuery.delete_insert_data( - delete_triples=validated_old_triples, - insert_triples=validated_new_triples, - where_clauses=old_triple_strings, - named_graph=named_graph, + def _render_term( + term: Any, + bn_map: Optional[Dict[BNode, str]] = None, + prefix: str = "", + ) -> str: + if isinstance(term, BNode): + if bn_map is None: + raise InvalidInputError( + "Blank nodes in predicates are not supported for updates." + ) + if term not in bn_map: + bn_map[term] = f"?{prefix}{len(bn_map) + 1}" + return bn_map[term] + if hasattr(term, "n3"): + return term.n3() + return IRI(term).n3() + + def _build_patterns( + triples: List[Triple], + bn_map: Dict[BNode, str], + prefix: str, + ) -> List[str]: + patterns: List[str] = [] + for subject, predicate, obj in triples: + subj_str = _render_term(subject, bn_map, prefix) + pred_str = _render_term(predicate) + obj_str = _render_term(obj, bn_map, prefix) + patterns.append(f"{subj_str} {pred_str} {obj_str} .") + return patterns + + old_bn_var_map: Dict[BNode, str] = {} + new_bn_var_map: Dict[BNode, str] = {} + + old_delete_patterns = _build_patterns( + validated_old_triples, old_bn_var_map, "oldbn" ) + insert_patterns = _build_patterns(validated_new_triples, new_bn_var_map, "newbn") + + def _format_block(patterns: List[str]) -> str: + if not patterns: + return "" + return "\n".join(f" {pattern}" for pattern in patterns) + + delete_block = _format_block(old_delete_patterns) + insert_block = _format_block(insert_patterns) + + where_patterns = list(dict.fromkeys(old_delete_patterns)) + where_block_parts: List[str] = [] + if where_patterns: + where_block_parts.append(_format_block(where_patterns)) + if new_bn_var_map: + where_block_parts.extend( + f" BIND(BNODE() AS {var})" for var in new_bn_var_map.values() + ) + where_block = "\n".join(where_block_parts) + + graph_clause = f"WITH {named_graph.n3()}\n" if named_graph else "" + + query = f"""{graph_clause}DELETE {{ +{delete_block} +}} +INSERT {{ +{insert_block} +}} +WHERE {{ +{where_block} +}} +""".strip() result = self.query(query=query, update=True) if not result: self.logger.warning( From 7b5f9b1b9b35f9f707eaa4648bddee9cf72c8140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Weindel?= Date: Wed, 25 Mar 2026 13:11:02 +0100 Subject: [PATCH 34/36] Fix merge error from feature_types - Partially reverting 'WIP: Fix any_triple_exists' (853feffe11ec0ee7a3c90a3434518d65a19446fc) - fixing tests for owl_get_classes_of_indiviidual --- .../queries/ontology_helpers.py | 14 ++++++------- graph_db_interface/queries/triple_multi.py | 8 ++++---- graph_db_interface/utils/utils.py | 20 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/graph_db_interface/queries/ontology_helpers.py b/graph_db_interface/queries/ontology_helpers.py index 78a2e83..c4a6e2f 100644 --- a/graph_db_interface/queries/ontology_helpers.py +++ b/graph_db_interface/queries/ontology_helpers.py @@ -1,7 +1,7 @@ # To be imported into ..graph_db.py GraphDB class import uuid -from typing import List, Optional, Set, Union, Callable, TYPE_CHECKING +from typing import List, Optional, Union, Callable, TYPE_CHECKING from graph_db_interface.utils import utils from graph_db_interface.utils.iri import IRI from graph_db_interface.utils.types import IRILike, GraphNameLike @@ -200,7 +200,7 @@ def owl_get_classes_of_individual( include_explicit: Optional[bool] = True, include_implicit: Optional[bool] = False, named_graph: Optional[GraphNameLike] = None, -) -> Set[Union[IRI, str]]: +) -> List[Union[IRI, str]]: """ Get the OWL classes associated with a given individual. @@ -217,7 +217,7 @@ def owl_get_classes_of_individual( named_graph (Optional[GraphNameLike]): Override the client's default named graph. Returns: - Set[Union[IRI, str]]: Class IRIs, or local names if `local_name=True`. + List[Union[IRI, str]]: Class IRIs, or local names if `local_name=True`. """ instance_iri = IRI(instance_iri) named_graph = IRI(named_graph) if named_graph is not None else self.named_graph @@ -253,14 +253,14 @@ def owl_get_classes_of_individual( results = self.query(query=query, convert_bindings=True) if results is None: - return set() + return [] if local_name: - classes = { + classes = [ utils.get_local_name(result["class"]) for result in results["results"]["bindings"] - } + ] return classes - classes = {result["class"] for result in results["results"]["bindings"]} + classes = [result["class"] for result in results["results"]["bindings"]] return classes diff --git a/graph_db_interface/queries/triple_multi.py b/graph_db_interface/queries/triple_multi.py index 1e4d28c..3e0d130 100644 --- a/graph_db_interface/queries/triple_multi.py +++ b/graph_db_interface/queries/triple_multi.py @@ -1,6 +1,6 @@ # To be imported into ..graph_db.py GraphDB class -from typing import Dict, List, Union, Any, Optional, Tuple, Set, TYPE_CHECKING +from typing import Dict, List, Union, Any, Optional, Tuple, TYPE_CHECKING from rdflib import BNode, Literal from graph_db_interface.utils import utils from graph_db_interface.utils.iri import IRI @@ -130,7 +130,7 @@ def any_triple_exists( if not triples: raise InvalidInputError(f"Cannot check existence of empty triple list.") - validated_triples = {utils.sanitize_triple(triple) for triple in triples} + validated_triples = [utils.sanitize_triple(triple) for triple in triples] triple_groups = utils.group_triples_by_bnode(validated_triples) @@ -348,8 +348,8 @@ def triples_update( if not old_triples and not new_triples: return True - # if len(old_triples) != len(new_triples): - # raise InvalidInputError("Old and new triples lists must have the same length.") + if len(old_triples) != len(new_triples): + raise InvalidInputError("Old and new triples lists must have the same length.") validated_old_triples = [utils.sanitize_triple(triple) for triple in old_triples] validated_new_triples = [utils.sanitize_triple(triple) for triple in new_triples] diff --git a/graph_db_interface/utils/utils.py b/graph_db_interface/utils/utils.py index b1229a4..10e410e 100644 --- a/graph_db_interface/utils/utils.py +++ b/graph_db_interface/utils/utils.py @@ -1,6 +1,6 @@ import logging import re -from typing import Set, Tuple, Union, Optional, Any, Dict +from typing import List, Union, Optional, Any, Dict from rdflib import Literal, XSD, Dataset, BNode, URIRef from rdflib.plugins.sparql.processor import prepareQuery from graph_db_interface.exceptions import ( @@ -344,16 +344,16 @@ def encapsulate_named_graph( return content -def group_triples_by_bnode(triples: Set[Triple]) -> Tuple[Set[Triple]]: +def group_triples_by_bnode(triples: List[Triple]) -> List[List[Triple]]: """ Groups triples sharing blank nodes into connected components to ensure blank node semantics are preserved within query scope. Args: - triples (Set[Triple]): Triples to check. + triples (List[Triple]): Triples to check. - Retruns: - Tuple[Set[Triple]]: A tuple of sets of related triples + Returns: + List[List[Triple]]: A list of lists of related triples Example: ASK { @@ -365,7 +365,7 @@ def group_triples_by_bnode(triples: Set[Triple]) -> Tuple[Set[Triple]]: { . } # Another independent triple } """ - triple_groups: Dict[BNode, Set[Triple]] = {} + triple_groups: Dict[BNode, List[Triple]] = {} for triple in triples: # Extract blank nodes in the triple @@ -377,7 +377,7 @@ def group_triples_by_bnode(triples: Set[Triple]) -> Tuple[Set[Triple]]: if not bnodes: # No blank nodes - independent triple # Use triple itself as hashable key - triple_groups[triple] = {triple} + triple_groups[triple] = [triple] continue containing_sets = [ @@ -385,7 +385,7 @@ def group_triples_by_bnode(triples: Set[Triple]) -> Tuple[Set[Triple]]: ] if not containing_sets: # Create new group - containing_set = set() + containing_set = [] else: # Existing groups - merge if multiple containing_set = containing_sets[0] @@ -393,9 +393,9 @@ def group_triples_by_bnode(triples: Set[Triple]) -> Tuple[Set[Triple]]: containing_set |= set_to_merge # Add triple to the containing group - containing_set.add(triple) + containing_set.append(triple) # Update bnode to group mapping - overwrite to merged group if needed for bnode in bnodes: triple_groups[bnode] = containing_set - return tuple(triple_groups.values()) + return list(triple_groups.values()) From becd8fb0994d0e966edeceafeecf83ed02b94952 Mon Sep 17 00:00:00 2001 From: JaFeKl Date: Wed, 25 Mar 2026 19:03:17 +0100 Subject: [PATCH 35/36] Fix CI repository selection in tests --- .github/workflows/tests.yml | 1 + poetry.lock | 738 +++++++++++++++++++++++++++++++----- tests/conftest.py | 6 +- 3 files changed, 656 insertions(+), 89 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f31a338..aadbe27 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,6 +16,7 @@ jobs: GRAPHDB_REPOSITORY: test-repo GRAPHDB_USERNAME: admin GRAPHDB_PASSWORD: root + GRAPHDB_TEST_REPOSITORY: Tests services: graphdb: diff --git a/poetry.lock b/poetry.lock index 869598e..98c416b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,16 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 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" +groups = ["main"] +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 = "astroid" @@ -12,9 +24,6 @@ files = [ {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} - [[package]] name = "black" version = "25.1.0" @@ -53,8 +62,6 @@ mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -62,6 +69,157 @@ d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"}, + {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"}, + {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"}, + {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"}, + {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"}, + {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"}, + {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"}, + {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"}, + {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"}, + {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"}, +] + [[package]] name = "click" version = "8.1.8" @@ -90,6 +248,152 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cramjam" +version = "2.11.0" +description = "Thin Python bindings to de/compression algorithms in Rust" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "cramjam-2.11.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d0859c65775e8ebf2cbc084bfd51bd0ffda10266da6f9306451123b89f8e5a63"}, + {file = "cramjam-2.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:1d77b9b0aca02a3f6eeeff27fcd315ca5972616c0919ee38e522cce257bcd349"}, + {file = "cramjam-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66425bc25b5481359b12a6719b6e7c90ffe76d85d0691f1da7df304bfb8ce45c"}, + {file = "cramjam-2.11.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd748d3407ec63e049b3aea1595e218814fccab329b7fb10bb51120a30e9fb7e"}, + {file = "cramjam-2.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6d9a23a35b3a105c42a8de60fc2e80281ae6e758f05a3baea0b68eb1ddcb679"}, + {file = "cramjam-2.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:40a75b95e05e38a2a055b2446f09994ce1139151721659315151d4ad6289bbff"}, + {file = "cramjam-2.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5d042c376d2025300da37d65192d06a457918b63b31140f697f85fd8e310b29"}, + {file = "cramjam-2.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb148b35ab20c75b19a06c27f05732e2a321adbd86fadc93f9466dbd7b1154a7"}, + {file = "cramjam-2.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ee47c220f0f5179ddc923ab91fc9e282c27b29fabc60c433dfe06f08084f798"}, + {file = "cramjam-2.11.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0cf1b5a81b21ea175c976c3ab09e00494258f4b49b7995efc86060cced3f0b2e"}, + {file = "cramjam-2.11.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:360c00338ecf48921492455007f904be607fc7818de3d681acbcc542aae2fb36"}, + {file = "cramjam-2.11.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f31fcc0d30dc3f3e94ea6b4d8e1a855071757c6abf6a7b1e284050ab7d4c299c"}, + {file = "cramjam-2.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:033be66fdceb3d63b2c99b257a98380c4ec22c9e4dca54a2bfec3718cd24e184"}, + {file = "cramjam-2.11.0-cp310-cp310-win32.whl", hash = "sha256:1c6cea67f6000b81f6bd27d14c8a6f62d00336ca7252fd03ee16f6b70eb5c0d2"}, + {file = "cramjam-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:98aa4a351b047b0f7f9e971585982065028adc2c162c5c23c5d5734c5ccc1077"}, + {file = "cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:04cfa39118570e70e920a9b75c733299784b6d269733dbc791d9aaed6edd2615"}, + {file = "cramjam-2.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:66a18f68506290349a256375d7aa2f645b9f7993c10fc4cc211db214e4e61d2b"}, + {file = "cramjam-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:50e7d65533857736cd56f6509cf2c4866f28ad84dd15b5bdbf2f8a81e77fa28a"}, + {file = "cramjam-2.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1f71989668458fc327ac15396db28d92df22f8024bb12963929798b2729d2df5"}, + {file = "cramjam-2.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee77ac543f1e2b22af1e8be3ae589f729491b6090582340aacd77d1d757d9569"}, + {file = "cramjam-2.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad52784120e7e4d8a0b5b0517d185b8bf7f74f5e17272857ddc8951a628d9be1"}, + {file = "cramjam-2.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b86f8e6d9c1b3f9a75b2af870c93ceee0f1b827cd2507387540e053b35d7459"}, + {file = "cramjam-2.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:320d61938950d95da2371b46c406ec433e7955fae9f396c8e1bf148ffc187d11"}, + {file = "cramjam-2.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41eafc8c1653a35a5c7e75ad48138f9f60085cc05cd99d592e5298552d944e9f"}, + {file = "cramjam-2.11.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03a7316c6bf763dfa34279335b27702321da44c455a64de58112968c0818ec4a"}, + {file = "cramjam-2.11.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:244c2ed8bd7ccbb294a2abe7ca6498db7e89d7eb5e744691dc511a7dc82e65ca"}, + {file = "cramjam-2.11.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:405f8790bad36ce0b4bbdb964ad51507bfc7942c78447f25cb828b870a1d86a0"}, + {file = "cramjam-2.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6b1b751a5411032b08fb3ac556160229ca01c6bbe4757bb3a9a40b951ebaac23"}, + {file = "cramjam-2.11.0-cp311-cp311-win32.whl", hash = "sha256:5251585608778b9ac8effed544933df7ad85b4ba21ee9738b551f17798b215ac"}, + {file = "cramjam-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:dca88bc8b68ce6d35dafd8c4d5d59a238a56c43fa02b74c2ce5f9dfb0d1ccb46"}, + {file = "cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dba5c14b8b4f73ea1e65720f5a3fe4280c1d27761238378be8274135c60bbc6e"}, + {file = "cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11eb40722b3fcf3e6890fba46c711bf60f8dc26360a24876c85e52d76c33b25b"}, + {file = "cramjam-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aeb26e2898994b6e8319f19a4d37c481512acdcc6d30e1b5ecc9d8ec57e835cb"}, + {file = "cramjam-2.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f8d82081ed7d8fe52c982bd1f06e4c7631a73fe1fb6d4b3b3f2404f87dc40fe"}, + {file = "cramjam-2.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:092a3ec26e0a679305018380e4f652eae1b6dfe3fc3b154ee76aa6b92221a17c"}, + {file = "cramjam-2.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:529d6d667c65fd105d10bd83d1cd3f9869f8fd6c66efac9415c1812281196a92"}, + {file = "cramjam-2.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:555eb9c90c450e0f76e27d9ff064e64a8b8c6478ab1a5594c91b7bc5c82fd9f0"}, + {file = "cramjam-2.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5edf4c9e32493035b514cf2ba0c969d81ccb31de63bd05490cc8bfe3b431674e"}, + {file = "cramjam-2.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2fe41f48c4d58d923803383b0737f048918b5a0d10390de9628bb6272b107"}, + {file = "cramjam-2.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9ca14cf1cabdb0b77d606db1bb9e9ca593b1dbd421fcaf251ec9a5431ec449f3"}, + {file = "cramjam-2.11.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:309e95bf898829476bccf4fd2c358ec00e7ff73a12f95a3cdeeba4bb1d3683d5"}, + {file = "cramjam-2.11.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:86dca35d2f15ef22922411496c220f3c9e315d5512f316fe417461971cc1648d"}, + {file = "cramjam-2.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:193c6488bd2f514cbc0bef5c18fad61a5f9c8d059dd56edf773b3b37f0e85496"}, + {file = "cramjam-2.11.0-cp312-cp312-win32.whl", hash = "sha256:514e2c008a8b4fa823122ca3ecab896eac41d9aa0f5fc881bd6264486c204e32"}, + {file = "cramjam-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:53fed080476d5f6ad7505883ec5d1ec28ba36c2273db3b3e92d7224fe5e463db"}, + {file = "cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2c289729cc1c04e88bafa48b51082fb462b0a57dbc96494eab2be9b14dca62af"}, + {file = "cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:045201ee17147e36cf43d8ae2fa4b4836944ac672df5874579b81cf6d40f1a1f"}, + {file = "cramjam-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:619cd195d74c9e1d2a3ad78d63451d35379c84bd851aec552811e30842e1c67a"}, + {file = "cramjam-2.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6eb3ae5ab72edb2ed68bdc0f5710f0a6cad7fd778a610ec2c31ee15e32d3921e"}, + {file = "cramjam-2.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7da3f4b19e3078f9635f132d31b0a8196accb2576e3213ddd7a77f93317c20"}, + {file = "cramjam-2.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57286b289cd557ac76c24479d8ecfb6c3d5b854cce54ccc7671f9a2f5e2a2708"}, + {file = "cramjam-2.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28952fbbf8b32c0cb7fa4be9bcccfca734bf0d0989f4b509dc7f2f70ba79ae06"}, + {file = "cramjam-2.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78ed2e4099812a438b545dfbca1928ec825e743cd253bc820372d6ef8c3adff4"}, + {file = "cramjam-2.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9aecd5c3845d415bd6c9957c93de8d93097e269137c2ecb0e5a5256374bdc8"}, + {file = "cramjam-2.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:362fcf4d6f5e1242a4540812455f5a594949190f6fbc04f2ffbfd7ae0266d788"}, + {file = "cramjam-2.11.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:13240b3dea41b1174456cb9426843b085dc1a2bdcecd9ee2d8f65ac5703374b0"}, + {file = "cramjam-2.11.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:c54eed83726269594b9086d827decc7d2015696e31b99bf9b69b12d9063584fe"}, + {file = "cramjam-2.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f8195006fdd0fc0a85b19df3d64a3ef8a240e483ae1dfc7ac6a4316019eb5df2"}, + {file = "cramjam-2.11.0-cp313-cp313-win32.whl", hash = "sha256:ccf30e3fe6d770a803dcdf3bb863fa44ba5dc2664d4610ba2746a3c73599f2e4"}, + {file = "cramjam-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee36348a204f0a68b03400f4736224e9f61d1c6a1582d7f875c1ca56f0254268"}, + {file = "cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7ba5e38c9fbd06f086f4a5a64a1a5b7b417cd3f8fc07a20e5c03651f72f36100"}, + {file = "cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8adeee57b41fe08e4520698a4b0bd3cc76dbd81f99424b806d70a5256a391d3"}, + {file = "cramjam-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b96a74fa03a636c8a7d76f700d50e9a8bc17a516d6a72d28711225d641e30968"}, + {file = "cramjam-2.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c3811a56fa32e00b377ef79121c0193311fd7501f0fb378f254c7f083cc1fbe0"}, + {file = "cramjam-2.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d927e87461f8a0d448e4ab5eb2bca9f31ca5d8ea86d70c6f470bb5bc666d7e"}, + {file = "cramjam-2.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f1f5c450121430fd89cb5767e0a9728ecc65997768fd4027d069cb0368af62f9"}, + {file = "cramjam-2.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:724aa7490be50235d97f07e2ca10067927c5d7f336b786ddbc868470e822aa25"}, + {file = "cramjam-2.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c4637122e7cfd7aac5c1d3d4c02364f446d6923ea34cf9d0e8816d6e7a4936"}, + {file = "cramjam-2.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17eb39b1696179fb471eea2de958fa21f40a2cd8bf6b40d428312d5541e19dc4"}, + {file = "cramjam-2.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:36aa5a798aa34e11813a80425a30d8e052d8de4a28f27bfc0368cfc454d1b403"}, + {file = "cramjam-2.11.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:449fca52774dc0199545fbf11f5128933e5a6833946707885cf7be8018017839"}, + {file = "cramjam-2.11.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:d87d37b3d476f4f7623c56a232045d25bd9b988314702ea01bd9b4a94948a778"}, + {file = "cramjam-2.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:26cb45c47d71982d76282e303931c6dd4baee1753e5d48f9a89b3a63e690b3a3"}, + {file = "cramjam-2.11.0-cp314-cp314-win32.whl", hash = "sha256:4efe919d443c2fd112fe25fe636a52f9628250c9a50d9bddb0488d8a6c09acc6"}, + {file = "cramjam-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccec3524ea41b9abd5600e3e27001fd774199dbb4f7b9cb248fcee37d4bda84c"}, + {file = "cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:966ac9358b23d21ecd895c418c048e806fd254e46d09b1ff0cdad2eba195ea3e"}, + {file = "cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:387f09d647a0d38dcb4539f8a14281f8eb6bb1d3e023471eb18a5974b2121c86"}, + {file = "cramjam-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:665b0d8fbbb1a7f300265b43926457ec78385200133e41fef19d85790fc1e800"}, + {file = "cramjam-2.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ca905387c7a371531b9622d93471be4d745ef715f2890c3702479cd4fc85aa51"}, + {file = "cramjam-2.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1aa56aef2c8af55a21ed39040a94a12b53fb23beea290f94d19a76027e2ffb"}, + {file = "cramjam-2.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5db59c1cdfaa2ab85cc988e602d6919495f735ca8a5fd7603608eb1e23c26d5"}, + {file = "cramjam-2.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f893014f00fe5e89a660a032e813bf9f6d91de74cd1490cdb13b2b59d0c9a3"}, + {file = "cramjam-2.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c26a1eb487947010f5de24943bd7c422dad955b2b0f8650762539778c380ca89"}, + {file = "cramjam-2.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d5c8bfb438d94e7b892d1426da5fc4b4a5370cc360df9b8d9d77c33b896c37e"}, + {file = "cramjam-2.11.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:cb1fb8c9337ab0da25a01c05d69a0463209c347f16512ac43be5986f3d1ebaf4"}, + {file = "cramjam-2.11.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:1f6449f6de52dde3e2f1038284910c8765a397a25e2d05083870f3f5e7fc682c"}, + {file = "cramjam-2.11.0-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:382dec4f996be48ed9c6958d4e30c2b89435d7c2c4dbf32480b3b8886293dd65"}, + {file = "cramjam-2.11.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:d388bd5723732c3afe1dd1d181e4213cc4e1be210b080572e7d5749f6e955656"}, + {file = "cramjam-2.11.0-cp314-cp314t-win32.whl", hash = "sha256:0a70ff17f8e1d13f322df616505550f0f4c39eda62290acb56f069d4857037c8"}, + {file = "cramjam-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:028400d699442d40dbda02f74158c73d05cb76587a12490d0bfedd958fd49188"}, + {file = "cramjam-2.11.0-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bf81b2e517baadf41eb85c4762ae596dd1dd2c852988ce86a2df6aa7e31d9228"}, + {file = "cramjam-2.11.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9f995c6b638255c9301166ed7033cb8fe0f34043a46b8e6a055a56b8a38c2114"}, + {file = "cramjam-2.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8949f97ab445d8aa2ccbeab244b46257114d38b6860210b2109b7e5b3ff2c5e"}, + {file = "cramjam-2.11.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b9a46eca804a51e8eb7b243c8e4513afc3b63aa60b69bc48e0efe6c648c4de0"}, + {file = "cramjam-2.11.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1789a057b6d09acf112c1c84701fc03ba5cc0fcf2ada786ce02a7e73dd466ca5"}, + {file = "cramjam-2.11.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:753710ae1f33b1a34178d104b7e1ac0a94a3f386d14dc24305663f63dc67cabc"}, + {file = "cramjam-2.11.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9115f7a4ba2f110e9dcda72a43adaeba202f42cf181877bcf3eecca359576bfe"}, + {file = "cramjam-2.11.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75b07d36ee034f05e3566878d83f3043d8297dad67937ada15c504ac3e50f9fd"}, + {file = "cramjam-2.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6a32313a5fdbc4fc4fd681a1895d55ee4bf81275e88638b1643b54ecf850cbe"}, + {file = "cramjam-2.11.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4526c4313306a264049e03e6c17b4e728a0647166dddbc1af7baf8e78a65721c"}, + {file = "cramjam-2.11.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:3705888b7acacddd46886926fa390dd3df0e1d9e6fe273fd4edb4cbf8eb64735"}, + {file = "cramjam-2.11.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72524cd27e67cf95d9c6bb5eacf47cf78473554f74685f57ccabb368988d91bc"}, + {file = "cramjam-2.11.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84265f2221e83fb1e41a8e33788c06e3ba22629e88644d0841a470cc28baa3f7"}, + {file = "cramjam-2.11.0-cp38-cp38-win32.whl", hash = "sha256:c77570660abcf3b8931b258d57b600b3484795977797009bed112f0d7b6933bf"}, + {file = "cramjam-2.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:20c8684d2a693e3052532b9730d4399ba2ee212cacf3b961349aad35d13b0c8c"}, + {file = "cramjam-2.11.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2581e82dca742b55d8b1d7f33892394c06b057a74f2853ffcb0802dcddcbf694"}, + {file = "cramjam-2.11.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a9994a42cd12f07ece04eff94dbf6e127b3986f7af9b26db1eb4545c477a6604"}, + {file = "cramjam-2.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4963dac24213690183110d6b41125fdc4af871a5a213589d6c6606d49e1b949"}, + {file = "cramjam-2.11.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c9af16f0b07d851b968c54e52d19430d820bb47c26d10a09cfb5c7127de26773"}, + {file = "cramjam-2.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e2400c09ba620e2ca91a903dbe907d75f6a1994d8337e9f3026778daa92b08d"}, + {file = "cramjam-2.11.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b820004db8b22715cee2ef154d4b47b3d76c4677ff217c587dd46f694a3052f9"}, + {file = "cramjam-2.11.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:261e9200942189d8201a005ffa1e29339479364b5b0013ab0758b03229d9ac67"}, + {file = "cramjam-2.11.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24c61f1fad56ca68aee53bf67b6a84cd762a2c71ee4b71064378547c2411ae6"}, + {file = "cramjam-2.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab86d22f69a21961f35d1a1b02278b5bb9a95c5f5b4722c6904bca343c8d219f"}, + {file = "cramjam-2.11.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a88bc9b191422cd5b22a1521b28607008590628b6b2a8a7db5c54ec04dc82fa1"}, + {file = "cramjam-2.11.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:7855bc4df5ed5f7fb1c98ea3fd98292e9acd3c097b1b21d596a69e1e60455400"}, + {file = "cramjam-2.11.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:19eb43e21db9dc42613599703c1a8e40b0170514a313f11f4c8be380425a1019"}, + {file = "cramjam-2.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cec977d673ad596bae6bdfc0091ee386cef05b515b23f2ce52f9fadd0156186a"}, + {file = "cramjam-2.11.0-cp39-cp39-win32.whl", hash = "sha256:dcc3b15b97f3054964b47e2a5fcfb4f5ff569e9af0a7af19f1d4c5f4231bbf3b"}, + {file = "cramjam-2.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:5eb0603d8f8019451fc00e1daf4022dfc9df59c16d2e68f925c77ac94555493b"}, + {file = "cramjam-2.11.0-pp310-pypy310_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:37bed927abc4a7ae2d2669baa3675e21904d8a038ed8e4313326ea7b3be62b2b"}, + {file = "cramjam-2.11.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:50e4a58635fa8c6897d84847d6e065eb69f92811670fc5e9f2d9e3b6279a02b6"}, + {file = "cramjam-2.11.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d1ba626dd5f81f7f09bbf59f70b534e2b75e0d6582b056b7bd31b397f1c13e9"}, + {file = "cramjam-2.11.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c71e140d5eb3145d61d59d0be0bf72f07cc4cf4b32cb136b09f712a3b1040f5f"}, + {file = "cramjam-2.11.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ed7926a5cca28edebad7d0fedd2ad492710ae3524d25fc59a2b20546d9ce1"}, + {file = "cramjam-2.11.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5eb4ed3cea945b164b0513fd491884993acac2153a27b93a84019c522e8eda82"}, + {file = "cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:52d5db3369f95b27b9f3c14d067acb0b183333613363ed34268c9e04560f997f"}, + {file = "cramjam-2.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4820516366d455b549a44d0e2210ee7c4575882dda677564ce79092588321d54"}, + {file = "cramjam-2.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d9e5db525dc0a950a825202f84ee68d89a072479e07da98795a3469df942d301"}, + {file = "cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62ab4971199b2270005359cdc379bc5736071dc7c9a228581c5122d9ffaac50c"}, + {file = "cramjam-2.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24758375cc5414d3035ca967ebb800e8f24604ececcba3c67d6f0218201ebf2d"}, + {file = "cramjam-2.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6c2eea545fef1065c7dd4eda991666fd9c783fbc1d226592ccca8d8891c02f23"}, + {file = "cramjam-2.11.0.tar.gz", hash = "sha256:5c82500ed91605c2d9781380b378397012e25127e89d64f460fea6aeac4389b4"}, +] + +[package.extras] +dev = ["black (==22.3.0)", "hypothesis (==6.60.0)", "numpy", "pytest (>=5.30)", "pytest-benchmark", "pytest-xdist"] + [[package]] name = "dill" version = "0.3.9" @@ -107,20 +411,19 @@ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] [[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version < \"3.11\"" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, ] [package.extras] -test = ["pytest (>=6)"] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "iniconfig" @@ -134,19 +437,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isodate" -version = "0.7.2" -description = "An ISO 8601 date/time/duration parser and formatter" -optional = false -python-versions = ">=3.7" -groups = ["main"] -markers = "python_version < \"3.11\"" -files = [ - {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, - {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, -] - [[package]] name = "isort" version = "6.0.1" @@ -244,6 +534,162 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pydantic" +version = "2.12.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + [[package]] name = "pylint" version = "3.3.4" @@ -257,19 +703,13 @@ files = [ ] [package.dependencies] -astroid = ">=3.3.8,<=3.4.0-dev0" +astroid = ">=3.3.8,<=3.4.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version == \"3.11\""}, -] +dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} isort = ">=4.2.5,<5.13.0 || >5.13.0,<7" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] @@ -304,15 +744,28 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "python-snappy" +version = "0.7.3" +description = "Python library for the snappy compression library from Google" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "python_snappy-0.7.3-py3-none-any.whl", hash = "sha256:074c0636cfcd97e7251330f428064050ac81a52c62ed884fc2ddebbb60ed7f50"}, + {file = "python_snappy-0.7.3.tar.gz", hash = "sha256:40216c1badfb2d38ac781ecb162a1d0ec40f8ee9747e610bcfefdfa79486cee3"}, +] + +[package.dependencies] +cramjam = "*" + [[package]] name = "rdflib" version = "7.1.3" @@ -326,7 +779,6 @@ files = [ ] [package.dependencies] -isodate = {version = ">=0.7.2,<1.0.0", markers = "python_version < \"3.11\""} pyparsing = ">=2.1.0,<4" [package.extras] @@ -337,48 +789,132 @@ networkx = ["networkx (>=2,<4)"] orjson = ["orjson (>=3.9.14,<4)"] [[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" +name = "regex" +version = "2024.11.6" +description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" +groups = ["main"] files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, ] +[[package]] +name = "requests" +version = "2.33.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b"}, + {file = "requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +test = ["PySocks (>=1.5.6,!=1.5.7)", "pytest (>=3)", "pytest-cov", "pytest-httpbin (==2.1.0)", "pytest-mock", "pytest-xdist"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + [[package]] name = "tomlkit" version = "0.13.2" @@ -393,18 +929,50 @@ files = [ [[package]] name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, ] +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + [metadata] lock-version = "2.1" -python-versions = "^3.9" -content-hash = "20e42b736c15c088ad7bbc6d71f0fd1d8ffe10419ed2f7cdb1a1b67a6a0f92d4" +python-versions = "^3.12" +content-hash = "e8a06fd55ef5b03b49fb0e472e8da78e57016e95c30532a6bf2af1f019ad9955" diff --git a/tests/conftest.py b/tests/conftest.py index c72cd6b..91bfc29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,8 +3,6 @@ import pytest from graph_db_interface import GraphDB, GraphDBCredentials -REPOSITORY = "Tests" - @pytest.fixture(scope="session") def db() -> GraphDB: @@ -13,7 +11,7 @@ def db() -> GraphDB: "GRAPHDB_URL", "GRAPHDB_USERNAME", "GRAPHDB_PASSWORD", - "GRAPHDB_REPOSITORY", + "GRAPHDB_TEST_REPOSITORY", ]: if os.getenv(env_var) is None: print(f"Missing environment variable '{env_var}'.", file=sys.stderr) @@ -23,7 +21,7 @@ def db() -> GraphDB: base_url=os.getenv("GRAPHDB_URL"), username=os.getenv("GRAPHDB_USERNAME"), password=os.getenv("GRAPHDB_PASSWORD"), - repository=REPOSITORY or os.getenv("GRAPHDB_REPOSITORY"), + repository=os.getenv("GRAPHDB_TEST_REPOSITORY"), ) db = GraphDB(credentials=credentials) From 44d12bf17a446be8134c915bc2f6846480b6227a Mon Sep 17 00:00:00 2001 From: JaFeKl Date: Wed, 25 Mar 2026 19:15:59 +0100 Subject: [PATCH 36/36] Fix environment variable handling in tests and update repository configuration --- .github/workflows/tests.yml | 7 +++---- tests/test_initialization.py | 4 +++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aadbe27..c41908c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,10 +13,9 @@ jobs: env: GRAPHDB_URL: http://localhost:7200 - GRAPHDB_REPOSITORY: test-repo GRAPHDB_USERNAME: admin GRAPHDB_PASSWORD: root - GRAPHDB_TEST_REPOSITORY: Tests + GRAPHDB_TEST_REPOSITORY: test-repo services: graphdb: @@ -59,14 +58,14 @@ jobs: - name: Create test repository run: | - cat > repo-config.ttl << 'EOF' + cat > repo-config.ttl << EOF @prefix rep: . @prefix sr: . @prefix sail: . @prefix owlim: . [] a rep:Repository ; - rep:repositoryID "test-repo" ; + rep:repositoryID "${GRAPHDB_TEST_REPOSITORY}" ; rdfs:label "Test Repository" ; rep:repositoryImpl [ rep:repositoryType "graphdb:SailRepository"; diff --git a/tests/test_initialization.py b/tests/test_initialization.py index f735abf..0218683 100644 --- a/tests/test_initialization.py +++ b/tests/test_initialization.py @@ -24,10 +24,12 @@ def test_credentials_from_environment(db: GraphDB): os.getenv("GRAPHDB_URL") is None or os.getenv("GRAPHDB_USERNAME") is None or os.getenv("GRAPHDB_PASSWORD") is None - or os.getenv("GRAPHDB_REPOSITORY") is None + or os.getenv("GRAPHDB_TEST_REPOSITORY") is None ): pytest.skip("One or more environment variables are not set") + os.environ["GRAPHDB_REPOSITORY"] = os.getenv("GRAPHDB_TEST_REPOSITORY") + try: credentials = GraphDBCredentials.from_env() GraphDB(credentials=credentials)