diff --git a/terminusdb_client/__init__.py b/terminusdb_client/__init__.py index 210459c1..76c84737 100644 --- a/terminusdb_client/__init__.py +++ b/terminusdb_client/__init__.py @@ -1,7 +1,8 @@ from .client import GraphType, Patch, Client # noqa from .woqldataframe import woqlDataframe as WOQLDataFrame # noqa -from .woqlquery import WOQLQuery, Var, Vars # noqa +from .woqlquery import WOQLQuery, Var, Vars # noqa from .woqlschema import * # noqa + # Backwards compatibility -WOQLClient = Client # noqa -WOQLSchema = Schema # noqa +WOQLClient = Client # noqa +WOQLSchema = Schema # noqa diff --git a/terminusdb_client/client/Client.py b/terminusdb_client/client/Client.py index 484122f1..c5844222 100644 --- a/terminusdb_client/client/Client.py +++ b/terminusdb_client/client/Client.py @@ -1,5 +1,6 @@ """Client.py Client is the Python public API for TerminusDB""" + import base64 import copy import gzip @@ -33,26 +34,27 @@ class WoqlResult: """Iterator for streaming WOQL results.""" + def __init__(self, lines): preface = json.loads(next(lines)) - if not ('@type' in preface and preface['@type'] == 'PrefaceRecord'): + if not ("@type" in preface and preface["@type"] == "PrefaceRecord"): raise DatabaseError(response=preface) self.preface = preface self.postscript = {} self.lines = lines def _check_error(self, document): - if ('@type' in document): - if document['@type'] == 'Binding': + if "@type" in document: + if document["@type"] == "Binding": return document - if document['@type'] == 'PostscriptRecord': + if document["@type"] == "PostscriptRecord": self.postscript = document raise StopIteration() raise DatabaseError(response=document) def variable_names(self): - return self.preface['names'] + return self.preface["names"] def __iter__(self): return self @@ -154,8 +156,9 @@ def copy(self): class GraphType(str, Enum): """Type of graph""" - INSTANCE = 'instance' - SCHEMA = 'schema' + + INSTANCE = "instance" + SCHEMA = "schema" class Client: @@ -397,7 +400,8 @@ def connect( def close(self) -> None: """Undo connect and close the connection. - The connection will be unusable from this point forward; an Error (or subclass) exception will be raised if any operation is attempted with the connection, unless connect is call again.""" + The connection will be unusable from this point forward; an Error (or subclass) exception will be raised if any operation is attempted with the connection, unless connect is call again. + """ self._connected = False def _check_connection(self, check_db=True) -> None: @@ -465,17 +469,17 @@ def ok(self) -> bool: if not self._connected: return self._connected req = self._session.get( - self.api + "/ok", - headers=self._default_headers, - timeout=6 + self.api + "/ok", headers=self._default_headers, timeout=6 ) return req.status_code == 200 - def log(self, - team: Optional[str] = None, - db: Optional[str] = None, - start: int = 0, - count: int = -1): + def log( + self, + team: Optional[str] = None, + db: Optional[str] = None, + start: int = 0, + count: int = -1, + ): """Get commit history of a database Parameters ---------- @@ -510,14 +514,14 @@ def log(self, db = db if db else self.db result = self._session.get( f"{self.api}/log/{team}/{db}", - params={'start': start, 'count': count}, + params={"start": start, "count": count}, headers=self._default_headers, auth=self._auth(), ) commits = json.loads(_finish_response(result)) for commit in commits: - commit['timestamp'] = datetime.fromtimestamp(commit['timestamp']) - commit['commit'] = commit['identifier'] # For backwards compat. + commit["timestamp"] = datetime.fromtimestamp(commit["timestamp"]) + commit["commit"] = commit["identifier"] # For backwards compat. return commits def get_commit_history(self, max_history: int = 500) -> list: @@ -639,14 +643,14 @@ def get_document_history( db = db if db else self.db params = { - 'id': doc_id, - 'start': start, - 'count': count, + "id": doc_id, + "start": start, + "count": count, } if created: - params['created'] = created + params["created"] = created if updated: - params['updated'] = updated + params["updated"] = updated result = self._session.get( f"{self.api}/history/{team}/{db}", @@ -660,24 +664,26 @@ def get_document_history( # Post-process timestamps from Unix timestamp to datetime objects if isinstance(history, list): for entry in history: - if 'timestamp' in entry and isinstance(entry['timestamp'], (int, float)): - entry['timestamp'] = datetime.fromtimestamp(entry['timestamp']) + if "timestamp" in entry and isinstance( + entry["timestamp"], (int, float) + ): + entry["timestamp"] = datetime.fromtimestamp(entry["timestamp"]) return history def _get_current_commit(self): descriptor = self.db if self.branch: - descriptor = f'{descriptor}/local/branch/{self.branch}' + descriptor = f"{descriptor}/local/branch/{self.branch}" commit = self.log(team=self.team, db=descriptor, count=1)[0] - return commit['identifier'] + return commit["identifier"] def _get_target_commit(self, step): descriptor = self.db if self.branch: - descriptor = f'{descriptor}/local/branch/{self.branch}' + descriptor = f"{descriptor}/local/branch/{self.branch}" commit = self.log(team=self.team, db=descriptor, count=1, start=step)[0] - return commit['identifier'] + return commit["identifier"] def get_all_branches(self, get_data_version=False): """Get all the branches available in the database.""" @@ -1105,7 +1111,9 @@ def get_triples(self, graph_type: GraphType) -> str: ) return json.loads(_finish_response(result)) - def update_triples(self, graph_type: GraphType, content: str, commit_msg: str) -> None: + def update_triples( + self, graph_type: GraphType, content: str, commit_msg: str + ) -> None: """Updates the contents of the specified graph with the triples encoded in turtle format. Replaces the entire graph contents @@ -1124,9 +1132,10 @@ def update_triples(self, graph_type: GraphType, content: str, commit_msg: str) - if the client does not connect to a database """ self._check_connection() - params = {"commit_info": self._generate_commit(commit_msg), - "turtle": content, - } + params = { + "commit_info": self._generate_commit(commit_msg), + "turtle": content, + } result = self._session.post( self._triples_url(graph_type), headers=self._default_headers, @@ -1155,9 +1164,7 @@ def insert_triples( if the client does not connect to a database """ self._check_connection() - params = {"commit_info": self._generate_commit(commit_msg), - "turtle": content - } + params = {"commit_info": self._generate_commit(commit_msg), "turtle": content} result = self._session.put( self._triples_url(graph_type), headers=self._default_headers, @@ -1318,9 +1325,15 @@ def get_documents_by_type( iterable Stream of dictionaries """ - return self.get_all_documents(graph_type, skip, count, - as_list, get_data_version, - doc_type=doc_type, **kwargs) + return self.get_all_documents( + graph_type, + skip, + count, + as_list, + get_data_version, + doc_type=doc_type, + **kwargs, + ) def get_all_documents( self, @@ -1361,11 +1374,14 @@ def get_all_documents( """ add_args = ["prefixed", "unfold"] self._check_connection() - payload = _args_as_payload({"graph_type": graph_type, - "skip": skip, - "type": doc_type, - "count": count, - }) + payload = _args_as_payload( + { + "graph_type": graph_type, + "skip": skip, + "type": doc_type, + "count": count, + } + ) for the_arg in add_args: if the_arg in kwargs: payload[the_arg] = kwargs[the_arg] @@ -1463,7 +1479,7 @@ def insert_document( commit_msg: Optional[str] = None, last_data_version: Optional[str] = None, compress: Union[str, int] = 1024, - raw_json: bool = False + raw_json: bool = False, ) -> None: """Inserts the specified document(s) @@ -1766,7 +1782,11 @@ def has_doc(self, doc_id: str, graph_type: GraphType = GraphType.INSTANCE) -> bo return True except DatabaseError as exception: body = exception.error_obj - if exception.status_code == 404 and "api:error" in body and body["api:error"]["@type"] == "api:DocumentNotFound": + if ( + exception.status_code == 404 + and "api:error" in body + and body["api:error"]["@type"] == "api:DocumentNotFound" + ): return False raise exception @@ -1851,7 +1871,7 @@ def query( headers=headers, json=query_obj, auth=self._auth(), - stream=streaming + stream=streaming, ) if streaming: @@ -1986,9 +2006,11 @@ def pull( return json.loads(_finish_response(result)) - def fetch(self, remote_id: str, - remote_auth: Optional[dict] = None, - ) -> dict: + def fetch( + self, + remote_id: str, + remote_auth: Optional[dict] = None, + ) -> dict: """Fetch the branch from a remote repo Parameters @@ -2062,7 +2084,13 @@ def push( "message": message, } if self._remote_auth_dict or remote_auth: - headers = {'Authorization-Remote' : self._generate_remote_header(remote_auth) if remote_auth else self._remote_auth()} + headers = { + "Authorization-Remote": ( + self._generate_remote_header(remote_auth) + if remote_auth + else self._remote_auth() + ) + } headers.update(self._default_headers) result = self._session.post( @@ -2299,12 +2327,9 @@ def _convert_diff_document(self, document): new_doc = self._conv_to_dict(document) return new_doc - def apply(self, - before_version, - after_version, - branch=None, - message=None, - author=None): + def apply( + self, before_version, after_version, branch=None, message=None, author=None + ): """Diff two different commits and apply changes on branch Parameters @@ -2349,8 +2374,7 @@ def diff_object(self, before_object, after_object): self._session.post( self._diff_url(), headers=self._default_headers, - json={'before': before_object, - 'after': after_object}, + json={"before": before_object, "after": after_object}, auth=self._auth(), ) ) @@ -2372,8 +2396,10 @@ def diff_version(self, before_version, after_version): self._session.post( self._diff_url(), headers=self._default_headers, - json={'before_data_version': before_version, - 'after_data_version': after_version}, + json={ + "before_data_version": before_version, + "after_data_version": after_version, + }, auth=self._auth(), ) ) @@ -2415,7 +2441,8 @@ def diff( >>> client = Client("http://127.0.0.1:6363/") >>> client.connect(user="admin", key="root", team="admin", db="some_db") >>> result = client.diff({ "@id" : "Person/Jane", "@type" : "Person", "name" : "Jane"}, { "@id" : "Person/Jane", "@type" : "Person", "name" : "Janine"}) - >>> result.to_json = '{ "name" : { "@op" : "SwapValue", "@before" : "Jane", "@after": "Janine" }}'""" + >>> result.to_json = '{ "name" : { "@op" : "SwapValue", "@before" : "Jane", "@after": "Janine" }}' + """ request_dict = {} for key, item in {"before": before, "after": after}.items(): @@ -2542,9 +2569,9 @@ def patch_resource( commit_info = self._generate_commit(message, author) request_dict = { "patch": patch.content, - "message" : commit_info["message"], - "author" : commit_info["author"], - "match_final_state" : match_final_state + "message": commit_info["message"], + "author": commit_info["author"], + "match_final_state": match_final_state, } patch_url = self._branch_base("patch", branch) @@ -2559,8 +2586,11 @@ def patch_resource( return json.loads(result) def clonedb( - self, clone_source: str, newid: str, description: Optional[str] = None, - remote_auth: Optional[dict] = None + self, + clone_source: str, + newid: str, + description: Optional[str] = None, + remote_auth: Optional[dict] = None, ) -> None: """Clone a remote repository and create a local copy. @@ -2590,7 +2620,13 @@ def clonedb( description = f"New database {newid}" if self._remote_auth_dict or remote_auth: - headers = {'Authorization-Remote' : self._generate_remote_header(remote_auth) if remote_auth else self._remote_auth()} + headers = { + "Authorization-Remote": ( + self._generate_remote_header(remote_auth) + if remote_auth + else self._remote_auth() + ) + } headers.update(self._default_headers) rc_args = {"remote_url": clone_source, "label": newid, "comment": description} @@ -2655,13 +2691,13 @@ def _remote_auth(self): return f"Token {token}" def _generate_remote_header(self, remote_auth: dict): - key_type = remote_auth['type'] - key = remote_auth['key'] - if key_type == 'http_basic': - username = remote_auth['username'] - http_basic_creds = base64.b64encode(f"{username}:{key}".encode('utf-8')) + key_type = remote_auth["type"] + key = remote_auth["key"] + if key_type == "http_basic": + username = remote_auth["username"] + http_basic_creds = base64.b64encode(f"{username}:{key}".encode("utf-8")) return f"Basic {http_basic_creds}" - elif key_type == 'token': + elif key_type == "token": return f"Token {key}" # JWT is the only key type remaining return f"Bearer {key}" @@ -2745,7 +2781,9 @@ def get_organization_user(self, org: str, username: str) -> Optional[dict]: ) return json.loads(_finish_response(result)) - def get_organization_user_databases(self, org: str, username: str) -> Optional[dict]: + def get_organization_user_databases( + self, org: str, username: str + ) -> Optional[dict]: """ Returns the databases available to a user which are inside an organization @@ -3363,7 +3401,9 @@ def _prefix_url(self, prefix_name: Optional[str] = None): """Get URL for prefix operations""" base = self._db_base("prefix") if self._db == "_system": - return base if prefix_name is None else f"{base}/{urlparse.quote(prefix_name)}" + return ( + base if prefix_name is None else f"{base}/{urlparse.quote(prefix_name)}" + ) # For regular databases, include repo and branch base = self._branch_base("prefix") return base if prefix_name is None else f"{base}/{urlparse.quote(prefix_name)}" diff --git a/terminusdb_client/query_syntax/__init__.py b/terminusdb_client/query_syntax/__init__.py index 150e2e44..2ee23ec4 100644 --- a/terminusdb_client/query_syntax/__init__.py +++ b/terminusdb_client/query_syntax/__init__.py @@ -1 +1 @@ -from .query_syntax import * # noqa +from .query_syntax import * # noqa diff --git a/terminusdb_client/query_syntax/query_syntax.py b/terminusdb_client/query_syntax/query_syntax.py index a1fc0e0e..fc7b0e57 100644 --- a/terminusdb_client/query_syntax/query_syntax.py +++ b/terminusdb_client/query_syntax/query_syntax.py @@ -1,11 +1,11 @@ -from ..woqlquery import WOQLQuery, Var, Vars, Doc # noqa +from ..woqlquery import WOQLQuery, Var, Vars, Doc # noqa import re import sys -__BARRED = ['re', 'vars'] -__ALLOWED = ['__and__', '__or__', '__add__'] +__BARRED = ["re", "vars"] +__ALLOWED = ["__and__", "__or__", "__add__"] __module = sys.modules[__name__] -__exported = ['Var', 'Vars', 'Doc'] +__exported = ["Var", "Vars", "Doc"] def __create_a_function(attribute): @@ -13,13 +13,16 @@ def __woql_fun(*args, **kwargs): obj = WOQLQuery() func = getattr(obj, attribute) return func(*args, **kwargs) + return __woql_fun for attribute in dir(WOQLQuery()): - if (isinstance(attribute, str) and (re.match('^[^_].*', attribute) - or attribute in __ALLOWED) - and attribute not in __BARRED): + if ( + isinstance(attribute, str) + and (re.match("^[^_].*", attribute) or attribute in __ALLOWED) + and attribute not in __BARRED + ): __exported.append(attribute) diff --git a/terminusdb_client/schema/schema.py b/terminusdb_client/schema/schema.py index 95fdd25c..e3595cc6 100644 --- a/terminusdb_client/schema/schema.py +++ b/terminusdb_client/schema/schema.py @@ -329,9 +329,9 @@ def _obj_to_dict(self, skip_checking=False): # object properties if hasattr(the_item, "_embedded_rep"): ref_obj = the_item._embedded_rep() - if '@ref' in ref_obj: - references[ref_obj['@ref']] = the_item - elif '@id' in ref_obj: + if "@ref" in ref_obj: + references[ref_obj["@ref"]] = the_item + elif "@id" in ref_obj: pass else: (sub_item, refs) = ref_obj @@ -345,9 +345,9 @@ def _obj_to_dict(self, skip_checking=False): # inner is object properties if hasattr(sub_item, "_embedded_rep"): ref_obj = sub_item._embedded_rep() - if '@ref' in ref_obj: - references[ref_obj['@ref']] = sub_item - elif '@id' in ref_obj: + if "@ref" in ref_obj: + references[ref_obj["@ref"]] = sub_item + elif "@id" in ref_obj: pass else: (sub_item, refs) = ref_obj @@ -740,7 +740,10 @@ def commit( commit_msg = "Schema object insert/ update by Python client." if full_replace: client.insert_document( - self, commit_msg=commit_msg, graph_type=GraphType.SCHEMA, full_replace=True + self, + commit_msg=commit_msg, + graph_type=GraphType.SCHEMA, + full_replace=True, ) else: client.update_document( @@ -1001,4 +1004,5 @@ class object: str or dict def copy(self): return deepcopy(self) -WOQLSchema = Schema # noqa + +WOQLSchema = Schema # noqa diff --git a/terminusdb_client/scripts/__main__.py b/terminusdb_client/scripts/__main__.py index c14e14b4..b4198f79 100644 --- a/terminusdb_client/scripts/__main__.py +++ b/terminusdb_client/scripts/__main__.py @@ -1,4 +1,4 @@ from terminusdb_client.scripts.scripts import tdbpy -if __name__ == '__main__': +if __name__ == "__main__": tdbpy() diff --git a/terminusdb_client/scripts/dev.py b/terminusdb_client/scripts/dev.py index 5e9b3471..19ecddbc 100644 --- a/terminusdb_client/scripts/dev.py +++ b/terminusdb_client/scripts/dev.py @@ -27,7 +27,9 @@ def run_command(cmd, cwd=None, check=True): """Run a shell command and return the result.""" print(f"Running: {' '.join(cmd)}") try: - result = subprocess.run(cmd, cwd=cwd, check=check, capture_output=True, text=True) + result = subprocess.run( + cmd, cwd=cwd, check=check, capture_output=True, text=True + ) if result.stdout: print(result.stdout) return result @@ -280,7 +282,9 @@ def pr(): # 2. Check formatting (don't fix) print("\nChecking code formatting...") try: - run_command(["poetry", "run", "black", "--check", "--diff", "terminusdb_client/"]) + run_command( + ["poetry", "run", "black", "--check", "--diff", "terminusdb_client/"] + ) print("✅ Black formatting is correct.") except subprocess.CalledProcessError: print("❌ Black formatting issues found.") diff --git a/terminusdb_client/tests/conftest.py b/terminusdb_client/tests/conftest.py index f288ffb9..ab654a1b 100644 --- a/terminusdb_client/tests/conftest.py +++ b/terminusdb_client/tests/conftest.py @@ -75,6 +75,7 @@ class Team(EnumTemplate): class Role(EnumTemplate): "Test Enum in a set" + _schema = my_schema Admin = () Read = () diff --git a/terminusdb_client/tests/integration_tests/test_client.py b/terminusdb_client/tests/integration_tests/test_client.py index 63997ae2..57fbb499 100644 --- a/terminusdb_client/tests/integration_tests/test_client.py +++ b/terminusdb_client/tests/integration_tests/test_client.py @@ -14,7 +14,7 @@ def test_not_ok(): - client = Client('http://localhost:6363') + client = Client("http://localhost:6363") not client.ok() @@ -112,7 +112,7 @@ def test_add_get_remove_org(docker_url): # test create db client.create_organization("testOrg") org = client.get_organization("testOrg") - assert org['name'] == 'testOrg' + assert org["name"] == "testOrg" client.delete_organization("testOrg") with pytest.raises(DatabaseError): # The org shouldn't exist anymore @@ -124,8 +124,8 @@ def test_diff_object(docker_url): client = Client(docker_url, user_agent=test_user_agent) # test create db client.connect() - diff = client.diff_object({'test': 'wew'}, {'test': 'wow'}) - assert diff == {'test': {'@before': 'wew', '@after': 'wow', '@op': 'SwapValue'}} + diff = client.diff_object({"test": "wew"}, {"test": "wow"}) + assert diff == {"test": {"@before": "wew", "@after": "wow", "@op": "SwapValue"}} def test_class_frame(docker_url): @@ -135,13 +135,13 @@ def test_class_frame(docker_url): client.create_database(db_name) client.connect(db=db_name) # Add a philosopher schema - schema = {"@type": "Class", - "@id": "Philosopher", - "name": "xsd:string" - } + schema = {"@type": "Class", "@id": "Philosopher", "name": "xsd:string"} # Add schema and Socrates client.insert_document(schema, graph_type=GraphType.SCHEMA) - assert client.get_class_frame("Philosopher") == {'@type': 'Class', 'name': 'xsd:string'} + assert client.get_class_frame("Philosopher") == { + "@type": "Class", + "name": "xsd:string", + } def test_woql_substr(docker_url): @@ -151,17 +151,16 @@ def test_woql_substr(docker_url): client.create_database(db_name) client.connect(db=db_name) # Add a philosopher schema - schema = {"@type": "Class", - "@id": "Philosopher", - "name": "xsd:string" - } + schema = {"@type": "Class", "@id": "Philosopher", "name": "xsd:string"} # Add schema and Socrates client.insert_document(schema, graph_type="schema") client.insert_document({"name": "Socrates"}) result = client.query( - WOQLQuery().triple('v:Philosopher', '@schema:name', 'v:Name') - .substr('v:Name', 3, 'v:Substring', 0, 'v:After')) - assert result['bindings'][0]['Substring']['@value'] == 'Soc' + WOQLQuery() + .triple("v:Philosopher", "@schema:name", "v:Name") + .substr("v:Name", 3, "v:Substring", 0, "v:After") + ) + assert result["bindings"][0]["Substring"]["@value"] == "Soc" def test_diff_apply_version(docker_url): @@ -171,10 +170,7 @@ def test_diff_apply_version(docker_url): client.create_database(db_name) client.connect(db=db_name) # Add a philosopher schema - schema = {"@type": "Class", - "@id": "Philosopher", - "name": "xsd:string" - } + schema = {"@type": "Class", "@id": "Philosopher", "name": "xsd:string"} # Add schema and Socrates client.insert_document(schema, graph_type="schema") client.insert_document({"name": "Socrates"}) @@ -189,11 +185,11 @@ def test_diff_apply_version(docker_url): diff = client.diff_version("main", "changes") assert len(diff) == 2 - assert diff[0]['@insert']['name'] == 'Plato' - assert diff[1]['@insert']['name'] == 'Aristotle' + assert diff[0]["@insert"]["name"] == "Plato" + assert diff[1]["@insert"]["name"] == "Aristotle" # Apply the differences to main with apply - client.apply("main", "changes", branch='main') + client.apply("main", "changes", branch="main") # Diff again diff_again = client.diff_version("main", "changes") @@ -208,7 +204,7 @@ def test_log(docker_url): db_name = "testDB" + str(random()) client.create_database(db_name, team="admin") log = client.log(team="admin", db=db_name) - assert log[0]['@type'] == 'InitialCommit' + assert log[0]["@type"] == "InitialCommit" def test_get_document_history(docker_url): @@ -226,7 +222,7 @@ def test_get_document_history(docker_url): "@type": "Class", "@id": "Person", "name": "xsd:string", - "age": "xsd:integer" + "age": "xsd:integer", } client.insert_document(schema, graph_type=GraphType.SCHEMA) @@ -249,14 +245,14 @@ def test_get_document_history(docker_url): # Assertions assert isinstance(history, list) assert len(history) >= 3 # At least insert and two updates - assert all('timestamp' in entry for entry in history) - assert all(isinstance(entry['timestamp'], dt.datetime) for entry in history) - assert all('author' in entry for entry in history) - assert all('message' in entry for entry in history) - assert all('identifier' in entry for entry in history) + assert all("timestamp" in entry for entry in history) + assert all(isinstance(entry["timestamp"], dt.datetime) for entry in history) + assert all("author" in entry for entry in history) + assert all("message" in entry for entry in history) + assert all("identifier" in entry for entry in history) # Verify messages are in the history (order may vary) - messages = [entry['message'] for entry in history] + messages = [entry["message"] for entry in history] assert "Created Person/Jane" in messages assert "Updated Person/Jane name and age" in messages assert "Updated Person/Jane age" in messages @@ -282,14 +278,14 @@ def test_get_triples(docker_url): client.create_database(db_name, team="admin") client.connect(db=db_name) # Add a philosopher schema - schema = {"@type": "Class", - "@id": "Philosopher", - "name": "xsd:string" - } + schema = {"@type": "Class", "@id": "Philosopher", "name": "xsd:string"} # Add schema and Socrates client.insert_document(schema, graph_type="schema") - schema_triples = client.get_triples(graph_type='schema') - assert "\n a sys:Class ;\n xsd:string ." in schema_triples + schema_triples = client.get_triples(graph_type="schema") + assert ( + "\n a sys:Class ;\n xsd:string ." + in schema_triples + ) def test_update_triples(docker_url): @@ -321,7 +317,7 @@ def test_update_triples(docker_url): db_name = "testDB" + str(random()) client.create_database(db_name, team="admin") client.connect(db=db_name) - client.update_triples(graph_type='schema', content=ttl, commit_msg="Update triples") + client.update_triples(graph_type="schema", content=ttl, commit_msg="Update triples") client.insert_document({"name": "Socrates"}) assert len(list(client.get_all_documents())) == 1 @@ -355,7 +351,7 @@ def test_insert_triples(docker_url): db_name = "testDB" + str(random()) client.create_database(db_name, team="admin") client.connect(db=db_name) - client.insert_triples(graph_type='schema', content=ttl, commit_msg="Insert triples") + client.insert_triples(graph_type="schema", content=ttl, commit_msg="Insert triples") client.insert_document({"name": "Socrates"}) assert len(list(client.get_all_documents())) == 1 @@ -366,9 +362,9 @@ def test_get_database(docker_url): db_name = "testDB" + str(random()) client.create_database(db_name, team="admin") db = client.get_database(db_name) - assert db['name'] == db_name - db_with_team = client.get_database(db_name, team='admin') - assert db_with_team['name'] == db_name + assert db["name"] == db_name + db_with_team = client.get_database(db_name, team="admin") + assert db_with_team["name"] == db_name with pytest.raises(DatabaseError): client.get_database("DOES_NOT_EXISTDB") @@ -398,30 +394,42 @@ def test_get_organization_user_databases(docker_url): client.create_database(db_name2, team=org_name) # BEFORE grant: verify our specific databases are NOT accessible to admin user - databases_before = client.get_organization_user_databases(org=org_name, username="admin") - db_names_before = {db['name'] for db in databases_before} - assert db_name not in db_names_before, f"{db_name} should not be accessible before capability grant" - assert db_name2 not in db_names_before, f"{db_name2} should not be accessible before capability grant" + databases_before = client.get_organization_user_databases( + org=org_name, username="admin" + ) + db_names_before = {db["name"] for db in databases_before} + assert ( + db_name not in db_names_before + ), f"{db_name} should not be accessible before capability grant" + assert ( + db_name2 not in db_names_before + ), f"{db_name2} should not be accessible before capability grant" # Grant capabilities to admin user for the organization capability_change = { "operation": "grant", "scope": f"Organization/{org_name}", "user": "User/admin", - "roles": [ - "Role/admin" - ] + "roles": ["Role/admin"], } client.change_capabilities(capability_change) # AFTER grant: verify our specific databases ARE accessible to admin user - databases_after = client.get_organization_user_databases(org=org_name, username="admin") - db_names_after = {db['name'] for db in databases_after} + databases_after = client.get_organization_user_databases( + org=org_name, username="admin" + ) + db_names_after = {db["name"] for db in databases_after} # Check both our databases are now accessible (order not guaranteed) - assert db_name in db_names_after, f"{db_name} should be accessible after capability grant" - assert db_name2 in db_names_after, f"{db_name2} should be accessible after capability grant" + assert ( + db_name in db_names_after + ), f"{db_name} should be accessible after capability grant" + assert ( + db_name2 in db_names_after + ), f"{db_name2} should be accessible after capability grant" # Verify admin team database does NOT appear in organization results - assert db_name + "admin" not in db_names_after, f"{db_name}admin should not appear in {org_name} results" + assert ( + db_name + "admin" not in db_names_after + ), f"{db_name}admin should not appear in {org_name} results" def test_has_database(docker_url): @@ -454,7 +462,7 @@ def test_add_get_remove_user(docker_url): # test create db client.add_user("test", "randomPassword") user = client.get_user("test") - assert user['name'] == 'test' + assert user["name"] == "test" client.delete_user("test") with pytest.raises(DatabaseError): user = client.get_user("test") @@ -465,12 +473,8 @@ def test_patch(docker_url): client = Client(docker_url, user_agent=test_user_agent) client.connect(user="admin", team="admin") client.create_database("patch") - schema = [{"@id" : "Person", - "@type" : "Class", - "name" : "xsd:string"}] - instance = [{"@type" : "Person", - "@id" : "Person/Jane", - "name" : "Jane"}] + schema = [{"@id": "Person", "@type": "Class", "name": "xsd:string"}] + instance = [{"@type": "Person", "@id": "Person/Jane", "name": "Jane"}] client.insert_document(schema, graph_type="schema") client.insert_document(instance) @@ -479,10 +483,8 @@ def test_patch(docker_url): ) client.patch_resource(patch) - doc = client.get_document('Person/Jane') - assert doc == {"@type" : "Person", - "@id" : "Person/Jane", - "name" : "Janine"} + doc = client.get_document("Person/Jane") + assert doc == {"@type": "Person", "@id": "Person/Jane", "name": "Janine"} client.delete_database("patch", "admin") @@ -605,7 +607,7 @@ def test_diff_ops_no_auth(test_schema, terminusx_token): @pytest.mark.skipif( os.environ.get("TERMINUSDB_TEST_JWT") is None, - reason="JWT testing not enabled. Set TERMINUSDB_TEST_JWT=1 to enable JWT tests." + reason="JWT testing not enabled. Set TERMINUSDB_TEST_JWT=1 to enable JWT tests.", ) def test_jwt(docker_url_jwt): # create client @@ -631,8 +633,10 @@ def test_jwt(docker_url_jwt): ) def test_terminusx(terminusx_token): testdb = ( - "test_happy_" + str(dt.datetime.now()).replace(" ", "") + "_" + str(random()) - ).replace(":", "_").replace(".", "") + ("test_happy_" + str(dt.datetime.now()).replace(" ", "") + "_" + str(random())) + .replace(":", "_") + .replace(".", "") + ) endpoint = "https://cloud-dev.terminusdb.com/TerminusDBTest/" client = Client(endpoint, user_agent=test_user_agent) client.connect(use_token=True, team="TerminusDBTest") @@ -652,8 +656,10 @@ def test_terminusx(terminusx_token): ) def test_terminusx_crazy_path(terminusx_token): testdb = ( - "test_crazy" + str(dt.datetime.now()).replace(" ", "") + "_" + str(random()) - ).replace(":", "_").replace(".", "") + ("test_crazy" + str(dt.datetime.now()).replace(" ", "") + "_" + str(random())) + .replace(":", "_") + .replace(".", "") + ) endpoint = "https://cloud-dev.terminusdb.com/TerminusDBTest/" client = Client(endpoint, user_agent=test_user_agent) client.connect(use_token=True, team="TerminusDBTest") diff --git a/terminusdb_client/tests/integration_tests/test_conftest.py b/terminusdb_client/tests/integration_tests/test_conftest.py index 61f9e728..26d4407f 100644 --- a/terminusdb_client/tests/integration_tests/test_conftest.py +++ b/terminusdb_client/tests/integration_tests/test_conftest.py @@ -1,4 +1,5 @@ """Unit tests for conftest.py helper functions""" + from unittest.mock import patch, Mock import requests @@ -12,7 +13,7 @@ class TestServerDetection: """Test server detection helper functions""" - @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + @patch("terminusdb_client.tests.integration_tests.conftest.requests.get") def test_local_server_running_200(self, mock_get): """Test local server detection returns True for HTTP 200""" mock_response = Mock() @@ -22,7 +23,7 @@ def test_local_server_running_200(self, mock_get): assert is_local_server_running() is True mock_get.assert_called_once_with("http://127.0.0.1:6363", timeout=2) - @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + @patch("terminusdb_client.tests.integration_tests.conftest.requests.get") def test_local_server_running_404(self, mock_get): """Test local server detection returns True for HTTP 404""" mock_response = Mock() @@ -31,21 +32,21 @@ def test_local_server_running_404(self, mock_get): assert is_local_server_running() is True - @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + @patch("terminusdb_client.tests.integration_tests.conftest.requests.get") def test_local_server_not_running_connection_error(self, mock_get): """Test local server detection returns False on connection error""" mock_get.side_effect = requests.exceptions.ConnectionError() assert is_local_server_running() is False - @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + @patch("terminusdb_client.tests.integration_tests.conftest.requests.get") def test_local_server_not_running_timeout(self, mock_get): """Test local server detection returns False on timeout""" mock_get.side_effect = requests.exceptions.Timeout() assert is_local_server_running() is False - @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + @patch("terminusdb_client.tests.integration_tests.conftest.requests.get") def test_docker_server_running_200(self, mock_get): """Test Docker server detection returns True for HTTP 200""" mock_response = Mock() @@ -55,7 +56,7 @@ def test_docker_server_running_200(self, mock_get): assert is_docker_server_running() is True mock_get.assert_called_once_with("http://127.0.0.1:6366", timeout=2) - @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + @patch("terminusdb_client.tests.integration_tests.conftest.requests.get") def test_docker_server_running_404(self, mock_get): """Test Docker server detection returns True for HTTP 404""" mock_response = Mock() @@ -64,14 +65,14 @@ def test_docker_server_running_404(self, mock_get): assert is_docker_server_running() is True - @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + @patch("terminusdb_client.tests.integration_tests.conftest.requests.get") def test_docker_server_not_running(self, mock_get): """Test Docker server detection returns False on connection error""" mock_get.side_effect = requests.exceptions.ConnectionError() assert is_docker_server_running() is False - @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + @patch("terminusdb_client.tests.integration_tests.conftest.requests.get") def test_jwt_server_running_200(self, mock_get): """Test JWT server detection returns True for HTTP 200""" mock_response = Mock() @@ -81,7 +82,7 @@ def test_jwt_server_running_200(self, mock_get): assert is_jwt_server_running() is True mock_get.assert_called_once_with("http://127.0.0.1:6367", timeout=2) - @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + @patch("terminusdb_client.tests.integration_tests.conftest.requests.get") def test_jwt_server_running_404(self, mock_get): """Test JWT server detection returns True for HTTP 404""" mock_response = Mock() @@ -90,7 +91,7 @@ def test_jwt_server_running_404(self, mock_get): assert is_jwt_server_running() is True - @patch('terminusdb_client.tests.integration_tests.conftest.requests.get') + @patch("terminusdb_client.tests.integration_tests.conftest.requests.get") def test_jwt_server_not_running(self, mock_get): """Test JWT server detection returns False on connection error""" mock_get.side_effect = requests.exceptions.ConnectionError() diff --git a/terminusdb_client/tests/integration_tests/test_prefix_management.py b/terminusdb_client/tests/integration_tests/test_prefix_management.py index c33f09ff..ec85ee1c 100644 --- a/terminusdb_client/tests/integration_tests/test_prefix_management.py +++ b/terminusdb_client/tests/integration_tests/test_prefix_management.py @@ -1,4 +1,5 @@ """Integration tests for prefix management operations.""" + import time import pytest @@ -19,7 +20,11 @@ def test_db(prefix_client): """Create and cleanup a test database.""" db_name = f"test_prefix_{int(time.time() * 1000)}" - prefix_client.create_database(db_name, label="Test Prefix DB", description="Database for testing prefix operations") + prefix_client.create_database( + db_name, + label="Test Prefix DB", + description="Database for testing prefix operations", + ) prefix_client.connect(db=db_name) yield db_name diff --git a/terminusdb_client/tests/integration_tests/test_schema.py b/terminusdb_client/tests/integration_tests/test_schema.py index 63cfe6d1..8cea3ffa 100644 --- a/terminusdb_client/tests/integration_tests/test_schema.py +++ b/terminusdb_client/tests/integration_tests/test_schema.py @@ -312,9 +312,9 @@ def test_repeated_object_load(docker_url, test_schema): client.insert_document( schema, commit_msg="I am checking in the schema", graph_type="schema" ) - [country_id] = client.insert_document({"@type" : "Country", - "name" : "Romania", - "perimeter" : []}) + [country_id] = client.insert_document( + {"@type": "Country", "name": "Romania", "perimeter": []} + ) obj = client.get_document(country_id) schema.import_objects(obj) obj2 = client.get_document(country_id) @@ -329,10 +329,13 @@ def test_key_change_raises_exception(docker_url, test_schema): client.insert_document( schema, commit_msg="I am checking in the schema", graph_type="schema" ) - [country_id] = client.insert_document({"@type" : "Country", - "name" : "Romania", - "perimeter" : []}) + [country_id] = client.insert_document( + {"@type": "Country", "name": "Romania", "perimeter": []} + ) obj = client.get_document(country_id) local_obj = schema.import_objects(obj) - with pytest.raises(ValueError, match=r"name has been used to generate the id, hence cannot be changed."): + with pytest.raises( + ValueError, + match=r"name has been used to generate the id, hence cannot be changed.", + ): local_obj.name = "France" diff --git a/terminusdb_client/tests/test_Client.py b/terminusdb_client/tests/test_Client.py index eeb28aba..e8431b1c 100644 --- a/terminusdb_client/tests/test_Client.py +++ b/terminusdb_client/tests/test_Client.py @@ -16,7 +16,7 @@ from .woqljson.woqlStarJson import WoqlStar -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_connection(mocked_requests): client = Client("http://localhost:6363") @@ -29,7 +29,7 @@ def test_connection(mocked_requests): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_user_agent_set(mocked_requests): client = Client("http://localhost:6363", user_agent="test_user_agent") @@ -42,7 +42,7 @@ def test_user_agent_set(mocked_requests): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_connected_flag(mocked_requests): client = Client("http://localhost:6363") assert not client._connected @@ -52,12 +52,10 @@ def test_connected_flag(mocked_requests): assert not client._connected -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_create_database(mocked_requests, mocked_requests2): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() assert client.user == "admin" @@ -72,13 +70,17 @@ def test_create_database(mocked_requests, mocked_requests2): client._session.post.assert_called_once_with( "http://localhost:6363/api/db/admin/myFirstTerminusDB", auth=("admin", "root"), - json={"label": "my first db", "comment": "my first db comment", "schema": False}, + json={ + "label": "my first db", + "comment": "my first db comment", + "schema": False, + }, headers={"user-agent": f"terminusdb-client-python/{__version__}"}, ) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) # @mock.patch("terminusdb_client.woqlclient.woqlClient.WOQLClient.create_graph") def test_create_database_with_schema(mocked_requests, mocked_requests2): client = Client("http://localhost:6363") @@ -98,8 +100,8 @@ def test_create_database_with_schema(mocked_requests, mocked_requests2): ) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_create_database_and_change_team(mocked_requests, mocked_requests2): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root") @@ -114,14 +116,18 @@ def test_create_database_and_change_team(mocked_requests, mocked_requests2): client._session.post.assert_called_once_with( "http://localhost:6363/api/db/my_new_team/myFirstTerminusDB", auth=("admin", "root"), - json={"label": "my first db", "comment": "my first db comment", "schema": False}, + json={ + "label": "my first db", + "comment": "my first db comment", + "schema": False, + }, headers={"user-agent": f"terminusdb-client-python/{__version__}"}, ) -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) def test_branch(mocked_requests, mocked_requests2, mocked_requests3): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root", db="myDBName") @@ -135,9 +141,9 @@ def test_branch(mocked_requests, mocked_requests2, mocked_requests3): ) -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) def test_crazy_branch(mocked_requests, mocked_requests2, mocked_requests3): client = Client("http://localhost:6363") client.connect(user="admin", team="amazing admin", key="root", db="my DB") @@ -151,8 +157,8 @@ def test_crazy_branch(mocked_requests, mocked_requests2, mocked_requests3): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) def test_get_database(mocked_requests, mocked_requests2): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root") @@ -166,8 +172,8 @@ def test_get_database(mocked_requests, mocked_requests2): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) def test_has_database(mocked_requests, mocked_requests2): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root") @@ -182,8 +188,8 @@ def test_has_database(mocked_requests, mocked_requests2): @pytest.mark.skip(reason="temporary not avaliable") -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_get_triples(mocked_requests, mocked_requests2): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root", db="myDBName") @@ -198,8 +204,8 @@ def test_get_triples(mocked_requests, mocked_requests2): @pytest.mark.skip(reason="temporary not avaliable") -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_get_triples_with_enum(mocked_requests, mocked_requests2): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root", db="myDBName") @@ -213,9 +219,9 @@ def test_get_triples_with_enum(mocked_requests, mocked_requests2): ) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_insert_delete) -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_insert_delete) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_query(mocked_requests, mocked_requests2, mocked_requests3): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root", db="myDBName") @@ -233,14 +239,14 @@ def test_query(mocked_requests, mocked_requests2, mocked_requests3): "message": "commit msg", }, "query": WoqlStar, - 'streaming': False + "streaming": False, }, headers={"user-agent": f"terminusdb-client-python/{__version__}"}, - stream=False + stream=False, ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_query_nodb(mocked_requests): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root") @@ -248,9 +254,9 @@ def test_query_nodb(mocked_requests): client.query(WoqlStar) -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_insert_delete) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_insert_delete) def test_query_commit_made(mocked_execute, mocked_requests, mocked_requests2): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root", db="myDBName") @@ -258,8 +264,8 @@ def test_query_commit_made(mocked_execute, mocked_requests, mocked_requests2): assert result == "Commit successfully made." -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_delete_database(mocked_requests, mocked_requests2): client = Client("http://localhost:6363") client.connect(user="admin", key="root", team="admin") @@ -276,7 +282,7 @@ def test_delete_database(mocked_requests, mocked_requests2): client.delete_database() -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_rollback(mocked_requests): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root") @@ -290,7 +296,7 @@ def test_copy_client(): assert id(client) != copy_client -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_basic_auth(mocked_requests): client = Client("http://localhost:6363") client.connect(user="admin", team="admin", key="root") @@ -299,19 +305,17 @@ def test_basic_auth(mocked_requests): assert client.user == "admin" -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_remote_auth(mocked_requests): client = Client("http://localhost:6363") auth_setting = {"type": "jwt", "user": "admin", "key": ""} - client.connect( - user="admin", team="admin", key="root", remote_auth=auth_setting - ) + client.connect(user="admin", team="admin", key="root", remote_auth=auth_setting) result = client._remote_auth_dict assert result == auth_setting -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_set_db(mocked_requests, mocked_requests2): client = Client("http://localhost:6363") with pytest.raises(InterfaceError): @@ -322,8 +326,8 @@ def test_set_db(mocked_requests, mocked_requests2): assert client.repo == "local" -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_full_replace_fail(mocked_requests, mocked_requests2): client = Client("http://localhost:6363") client.connect(db="myDBName") @@ -333,8 +337,8 @@ def test_full_replace_fail(mocked_requests, mocked_requests2): ) -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_insert_woqlschema_fail(mocked_requests, mocked_requests2): client = Client("http://localhost:6363") client.connect(db="myDBName") @@ -342,15 +346,13 @@ def test_insert_woqlschema_fail(mocked_requests, mocked_requests2): client.insert_document(WOQLSchema(), graph_type="instance") -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'delete', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "delete", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_delete_document( mocked_requests, mocked_requests2, mocked_requests3, test_schema ): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect(db="myDBName") client.delete_document(["id1", "id2"]) @@ -412,12 +414,10 @@ def test_delete_document( ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) def test_add_user(mocked_requests, mocked_requests2): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() user = "Gavin" + str(random.randrange(100000)) @@ -431,13 +431,11 @@ def test_add_user(mocked_requests, mocked_requests2): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'delete', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "delete", side_effect=mocked_request_success) def test_delete_user(mocked_requests, mocked_requests2, mocked_requests3): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() user = "Gavin" + str(random.randrange(100000)) @@ -451,13 +449,11 @@ def test_delete_user(mocked_requests, mocked_requests2, mocked_requests3): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'delete', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "delete", side_effect=mocked_request_success) def test_delete_organization(mocked_requests, mocked_requests2, mocked_requests3): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() org = "RandomOrg" + str(random.randrange(100000)) @@ -471,13 +467,11 @@ def test_delete_organization(mocked_requests, mocked_requests2, mocked_requests3 ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'put', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "put", side_effect=mocked_request_success) def test_change_user_password(mocked_requests, mocked_requests2, mocked_requests3): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() user = "Gavin" + str(random.randrange(100000)) @@ -492,12 +486,10 @@ def test_change_user_password(mocked_requests, mocked_requests2, mocked_requests ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) def test_create_organization(mocked_requests, mocked_requests2): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() org = "RandomOrg" + str(random.randrange(100000)) @@ -510,11 +502,9 @@ def test_create_organization(mocked_requests, mocked_requests2): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_get_organization_users(mocked_requests): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() client.get_organization_users("admin") client._session.get.assert_called_with( @@ -524,11 +514,9 @@ def test_get_organization_users(mocked_requests): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_get_organization_user(mocked_requests): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() client.get_organization_user("admin", "admin") client._session.get.assert_called_with( @@ -538,11 +526,9 @@ def test_get_organization_user(mocked_requests): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_get_organizations(mocked_requests): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() client.get_organizations() client._session.get.assert_called_with( @@ -552,21 +538,16 @@ def test_get_organizations(mocked_requests): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) def test_capabilities_change(mocked_requests, mocked_requests2): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() capability_change = { "operation": "revoke", "scope": "UserDatabase/f5a0ef94469b32e1aee321678436c7dfd5a96d9c476672b3282ae89a45b5200e", "user": "User/admin", - "roles": [ - "Role/consumer", - "Role/admin" - ] + "roles": ["Role/consumer", "Role/admin"], } try: client.change_capabilities(capability_change) @@ -580,12 +561,10 @@ def test_capabilities_change(mocked_requests, mocked_requests2): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) def test_add_role(mocked_requests, mocked_requests2): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() role = { "name": "Grand Pubah", @@ -606,8 +585,8 @@ def test_add_role(mocked_requests, mocked_requests2): "push", "rebase", "schema_read_access", - "schema_write_access" - ] + "schema_write_access", + ], } try: client.add_role(role) @@ -621,13 +600,11 @@ def test_add_role(mocked_requests, mocked_requests2): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'put', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "put", side_effect=mocked_request_success) def test_change_role(mocked_requests, mocked_requests2, mocked_requests3): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() role = { "name": "Grand Pubahz", @@ -648,12 +625,12 @@ def test_change_role(mocked_requests, mocked_requests2, mocked_requests3): "push", "rebase", "schema_read_access", - "schema_write_access" - ] + "schema_write_access", + ], } try: client.add_role(role) - del role['action'][2] # Delete clone as action + del role["action"][2] # Delete clone as action client.change_role(role) except InterfaceError: pass @@ -665,11 +642,9 @@ def test_change_role(mocked_requests, mocked_requests2, mocked_requests3): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_get_roles(mocked_requests): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() try: client.get_available_roles() @@ -682,11 +657,9 @@ def test_get_roles(mocked_requests): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_get_users(mocked_requests): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() try: client.get_users() @@ -699,11 +672,9 @@ def test_get_users(mocked_requests): ) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_get_user(mocked_requests): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect() user = "Gavin" + str(random.randrange(100000)) client.get_user(user) @@ -714,12 +685,10 @@ def test_get_user(mocked_requests): ) -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_get_document_history(mocked_get, mocked_head): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect(db="myDBName") client.get_document_history("Person/Jane", start=0, count=10) @@ -728,16 +697,16 @@ def test_get_document_history(mocked_get, mocked_head): last_call = client._session.get.call_args_list[-1] assert last_call[0][0] == "http://localhost:6363/api/history/admin/myDBName" assert last_call[1]["params"] == {"id": "Person/Jane", "start": 0, "count": 10} - assert last_call[1]["headers"] == {"user-agent": f"terminusdb-client-python/{__version__}"} + assert last_call[1]["headers"] == { + "user-agent": f"terminusdb-client-python/{__version__}" + } assert last_call[1]["auth"] == ("admin", "root") -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_get_document_history_with_created_updated(mocked_get, mocked_head): - client = Client( - "http://localhost:6363", user="admin", key="root", team="admin" - ) + client = Client("http://localhost:6363", user="admin", key="root", team="admin") client.connect(db="myDBName") client.get_document_history("Person/Jane", created=True, updated=True) @@ -745,6 +714,14 @@ def test_get_document_history_with_created_updated(mocked_get, mocked_head): # Get the last call to get (should be our get_document_history call) last_call = client._session.get.call_args_list[-1] assert last_call[0][0] == "http://localhost:6363/api/history/admin/myDBName" - assert last_call[1]["params"] == {"id": "Person/Jane", "start": 0, "count": 10, "created": True, "updated": True} - assert last_call[1]["headers"] == {"user-agent": f"terminusdb-client-python/{__version__}"} + assert last_call[1]["params"] == { + "id": "Person/Jane", + "start": 0, + "count": 10, + "created": True, + "updated": True, + } + assert last_call[1]["headers"] == { + "user-agent": f"terminusdb-client-python/{__version__}" + } assert last_call[1]["auth"] == ("admin", "root") diff --git a/terminusdb_client/tests/test_Schema.py b/terminusdb_client/tests/test_Schema.py index e6dcaf78..0fd41e95 100644 --- a/terminusdb_client/tests/test_Schema.py +++ b/terminusdb_client/tests/test_Schema.py @@ -198,35 +198,29 @@ def test_embedded_object(test_schema): street="test", country=Country(name="Republic of Ireland"), ), - friend_of={Person( - name="Katy", - age=51 - )} + friend_of={Person(name="Katy", age=51)}, ) client = Client("http://127.0.0.1:6366") - result = client._convert_document(gavin, 'instance') + result = client._convert_document(gavin, "instance") # Finds the internal object and splays it out properly - assert (len(result) == 2) + assert len(result) == 2 def test_person_sys_json(): schema_json = { "@id": "PersonJSONTest", - "@key": { - "@type": "Random" - }, + "@key": {"@type": "Random"}, "@type": "Class", - "metadata": { - "@class": "sys:JSON", - "@type": "Optional" - } + "metadata": {"@class": "sys:JSON", "@type": "Optional"}, } schema = Schema() test_result = schema._construct_class(schema_json) - result = {'@id': 'PersonJSONTest', - '@key': {'@type': 'Random'}, - '@type': 'Class', - 'metadata': {'@class': 'sys:JSON', '@type': 'Optional'}} + result = { + "@id": "PersonJSONTest", + "@key": {"@type": "Random"}, + "@type": "Class", + "metadata": {"@class": "sys:JSON", "@type": "Optional"}, + } assert test_result._to_dict() == result @@ -324,10 +318,10 @@ def test_datetime(): assert new_obj.duration == delta -@mock.patch.object(requests.Session, 'head', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'post', side_effect=mocked_request_success) -@mock.patch.object(requests.Session, 'put', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "head", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "post", side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "put", side_effect=mocked_request_success) def test_compress_data(patched, patched2, patched3, patched4): datetime_obj = dt.datetime(2019, 5, 18, 15, 17, 8, 132263) delta = dt.timedelta( @@ -457,6 +451,7 @@ def test_from_json_schema(): class AnEnum(EnumTemplate): "An enum" + _schema = Schema() Foo = () Bar = () diff --git a/terminusdb_client/tests/test_backwardsCompat.py b/terminusdb_client/tests/test_backwardsCompat.py index 2fda8e66..4fc96bd3 100644 --- a/terminusdb_client/tests/test_backwardsCompat.py +++ b/terminusdb_client/tests/test_backwardsCompat.py @@ -9,7 +9,7 @@ from terminusdb_client.woqlclient import WOQLClient -@mock.patch.object(requests.Session, 'get', side_effect=mocked_request_success) +@mock.patch.object(requests.Session, "get", side_effect=mocked_request_success) def test_connection(mocked_requests): client = WOQLClient("http://localhost:6363") diff --git a/terminusdb_client/tests/test_client_init.py b/terminusdb_client/tests/test_client_init.py index 62e2af0a..a179e69c 100644 --- a/terminusdb_client/tests/test_client_init.py +++ b/terminusdb_client/tests/test_client_init.py @@ -1,4 +1,5 @@ """Unit tests for Client initialization""" + from terminusdb_client.client import Client diff --git a/terminusdb_client/tests/test_errors.py b/terminusdb_client/tests/test_errors.py index 69a3b716..d702430c 100644 --- a/terminusdb_client/tests/test_errors.py +++ b/terminusdb_client/tests/test_errors.py @@ -1,4 +1,5 @@ """Tests for errors.py module.""" + import json from unittest.mock import Mock from terminusdb_client.errors import ( @@ -8,7 +9,7 @@ OperationalError, AccessDeniedError, APIError, - InvalidURIError + InvalidURIError, ) @@ -32,7 +33,7 @@ def test_interface_error_string_representation(): message = "Connection failed" error = InterfaceError(message) - assert hasattr(error, 'message') + assert hasattr(error, "message") assert error.message == message @@ -55,7 +56,7 @@ def test_database_error_with_json_api_message(): mock_response.headers = {"content-type": "application/json"} mock_response.json.return_value = { "api:message": "Database operation failed", - "details": "Additional context" + "details": "Additional context", } mock_response.status_code = 400 @@ -72,9 +73,7 @@ def test_database_error_with_vio_message(): mock_response.text = "Error response" mock_response.headers = {"content-type": "application/json"} mock_response.json.return_value = { - "api:error": { - "vio:message": "Validation failed" - } + "api:error": {"vio:message": "Validation failed"} } mock_response.status_code = 422 @@ -89,9 +88,7 @@ def test_database_error_with_unknown_json(): mock_response = Mock() mock_response.text = "Error response" mock_response.headers = {"content-type": "application/json"} - mock_response.json.return_value = { - "unknown_field": "some value" - } + mock_response.json.return_value = {"unknown_field": "some value"} mock_response.status_code = 500 error = DatabaseError(mock_response) @@ -185,10 +182,7 @@ def test_database_error_json_formatting(): mock_response = Mock() mock_response.text = "Error" mock_response.headers = {"content-type": "application/json"} - error_data = { - "api:message": "Error message", - "code": "ERR_001" - } + error_data = {"api:message": "Error message", "code": "ERR_001"} mock_response.json.return_value = error_data mock_response.status_code = 400 @@ -201,11 +195,7 @@ def test_database_error_json_formatting(): def test_all_error_classes_are_exceptions(): """Test that all error classes inherit from Exception.""" - errors = [ - Error(), - InterfaceError("test"), - InvalidURIError() - ] + errors = [Error(), InterfaceError("test"), InvalidURIError()] for error in errors: assert isinstance(error, Exception) diff --git a/terminusdb_client/tests/test_graphtype.py b/terminusdb_client/tests/test_graphtype.py index a32347d4..025c8801 100644 --- a/terminusdb_client/tests/test_graphtype.py +++ b/terminusdb_client/tests/test_graphtype.py @@ -1,4 +1,5 @@ """Unit tests for GraphType enum""" + from terminusdb_client.client import GraphType @@ -22,5 +23,5 @@ def test_graphtype_enum_members(self): def test_graphtype_values(self): """Test all GraphType enum values exist""" - assert hasattr(GraphType, 'INSTANCE') - assert hasattr(GraphType, 'SCHEMA') + assert hasattr(GraphType, "INSTANCE") + assert hasattr(GraphType, "SCHEMA") diff --git a/terminusdb_client/tests/test_query_syntax.py b/terminusdb_client/tests/test_query_syntax.py index e94a8b93..20e5abe5 100644 --- a/terminusdb_client/tests/test_query_syntax.py +++ b/terminusdb_client/tests/test_query_syntax.py @@ -1,43 +1,44 @@ """Tests for query_syntax/query_syntax.py module.""" + from terminusdb_client.query_syntax import query_syntax def test_query_syntax_exports_var(): """Test that Var is exported.""" - assert 'Var' in query_syntax.__all__ - assert hasattr(query_syntax, 'Var') + assert "Var" in query_syntax.__all__ + assert hasattr(query_syntax, "Var") def test_query_syntax_exports_vars(): """Test that Vars is exported.""" - assert 'Vars' in query_syntax.__all__ - assert hasattr(query_syntax, 'Vars') + assert "Vars" in query_syntax.__all__ + assert hasattr(query_syntax, "Vars") def test_query_syntax_exports_doc(): """Test that Doc is exported.""" - assert 'Doc' in query_syntax.__all__ - assert hasattr(query_syntax, 'Doc') + assert "Doc" in query_syntax.__all__ + assert hasattr(query_syntax, "Doc") def test_barred_items_not_exported(): """Test that barred items (re, vars) are not exported.""" - assert 're' not in query_syntax.__all__ - assert 'vars' not in query_syntax.__all__ + assert "re" not in query_syntax.__all__ + assert "vars" not in query_syntax.__all__ def test_allowed_dunder_methods_exported(): """Test that allowed dunder methods are exported.""" # __and__, __or__, __add__ should be exported - assert '__and__' in query_syntax.__all__ - assert '__or__' in query_syntax.__all__ - assert '__add__' in query_syntax.__all__ + assert "__and__" in query_syntax.__all__ + assert "__or__" in query_syntax.__all__ + assert "__add__" in query_syntax.__all__ def test_dynamic_function_creation(): """Test that functions are dynamically created from WOQLQuery.""" # Check that some common WOQLQuery methods are available - woql_methods = ['triple', 'select', 'limit'] + woql_methods = ["triple", "select", "limit"] for method in woql_methods: assert hasattr(query_syntax, method), f"{method} should be available" @@ -47,16 +48,16 @@ def test_dynamic_function_creation(): def test_created_functions_are_callable(): """Test that dynamically created functions are callable.""" # Test that we can call a dynamically created function - if hasattr(query_syntax, 'limit'): - func = getattr(query_syntax, 'limit') + if hasattr(query_syntax, "limit"): + func = getattr(query_syntax, "limit") assert callable(func) def test_created_function_returns_woqlquery(): """Test that created functions return WOQLQuery instances.""" # Test a simple function that exists on WOQLQuery - if hasattr(query_syntax, 'limit'): - func = getattr(query_syntax, 'limit') + if hasattr(query_syntax, "limit"): + func = getattr(query_syntax, "limit") result = func(10) # Should return a WOQLQuery or similar object assert result is not None @@ -66,14 +67,14 @@ def test_private_attributes_not_exported(): """Test that private attributes (starting with _) are not exported.""" for attr in query_syntax.__all__: # All exported attributes should either be in __ALLOWED or not start with _ - if attr not in ['__and__', '__or__', '__add__']: - assert not attr.startswith('_'), f"{attr} should not start with underscore" + if attr not in ["__and__", "__or__", "__add__"]: + assert not attr.startswith("_"), f"{attr} should not start with underscore" def test_module_attributes(): """Test module has expected attributes.""" # These are defined in the module - assert hasattr(query_syntax, '__BARRED') - assert hasattr(query_syntax, '__ALLOWED') - assert hasattr(query_syntax, '__module') - assert hasattr(query_syntax, '__exported') + assert hasattr(query_syntax, "__BARRED") + assert hasattr(query_syntax, "__ALLOWED") + assert hasattr(query_syntax, "__module") + assert hasattr(query_syntax, "__exported") diff --git a/terminusdb_client/tests/test_schema_template.py b/terminusdb_client/tests/test_schema_template.py index f2405bd7..0b403522 100644 --- a/terminusdb_client/tests/test_schema_template.py +++ b/terminusdb_client/tests/test_schema_template.py @@ -1,4 +1,5 @@ """Tests for scripts/schema_template.py module.""" + from terminusdb_client.scripts import schema_template from terminusdb_client.woqlschema import DocumentTemplate, EnumTemplate, TaggedUnion @@ -11,98 +12,98 @@ def test_schema_template_imports(): def test_country_class_exists(): """Test that Country class is defined.""" - assert hasattr(schema_template, 'Country') + assert hasattr(schema_template, "Country") assert issubclass(schema_template.Country, DocumentTemplate) def test_country_has_key(): """Test that Country has HashKey configured.""" - assert hasattr(schema_template.Country, '_key') + assert hasattr(schema_template.Country, "_key") def test_country_attributes(): """Test that Country has expected attributes.""" country = schema_template.Country # Check type hints exist - assert hasattr(country, '__annotations__') - assert 'name' in country.__annotations__ - assert 'also_know_as' in country.__annotations__ + assert hasattr(country, "__annotations__") + assert "name" in country.__annotations__ + assert "also_know_as" in country.__annotations__ def test_address_class_exists(): """Test that Address class is defined.""" - assert hasattr(schema_template, 'Address') + assert hasattr(schema_template, "Address") assert issubclass(schema_template.Address, DocumentTemplate) def test_address_is_subdocument(): """Test that Address is configured as subdocument.""" - assert hasattr(schema_template.Address, '_subdocument') + assert hasattr(schema_template.Address, "_subdocument") def test_address_attributes(): """Test that Address has expected attributes.""" address = schema_template.Address - assert 'street' in address.__annotations__ - assert 'postal_code' in address.__annotations__ - assert 'country' in address.__annotations__ + assert "street" in address.__annotations__ + assert "postal_code" in address.__annotations__ + assert "country" in address.__annotations__ def test_person_class_exists(): """Test that Person class is defined.""" - assert hasattr(schema_template, 'Person') + assert hasattr(schema_template, "Person") assert issubclass(schema_template.Person, DocumentTemplate) def test_person_has_docstring(): """Test that Person has numpydoc formatted docstring.""" assert schema_template.Person.__doc__ is not None - assert 'Attributes' in schema_template.Person.__doc__ + assert "Attributes" in schema_template.Person.__doc__ def test_person_attributes(): """Test that Person has expected attributes.""" person = schema_template.Person - assert 'name' in person.__annotations__ - assert 'age' in person.__annotations__ - assert 'friend_of' in person.__annotations__ + assert "name" in person.__annotations__ + assert "age" in person.__annotations__ + assert "friend_of" in person.__annotations__ def test_employee_inherits_person(): """Test that Employee inherits from Person.""" - assert hasattr(schema_template, 'Employee') + assert hasattr(schema_template, "Employee") assert issubclass(schema_template.Employee, schema_template.Person) def test_employee_attributes(): """Test that Employee has expected attributes.""" employee = schema_template.Employee - assert 'address_of' in employee.__annotations__ - assert 'contact_number' in employee.__annotations__ - assert 'managed_by' in employee.__annotations__ + assert "address_of" in employee.__annotations__ + assert "contact_number" in employee.__annotations__ + assert "managed_by" in employee.__annotations__ def test_coordinate_class_exists(): """Test that Coordinate class is defined.""" - assert hasattr(schema_template, 'Coordinate') + assert hasattr(schema_template, "Coordinate") assert issubclass(schema_template.Coordinate, DocumentTemplate) def test_coordinate_is_abstract(): """Test that Coordinate is configured as abstract.""" - assert hasattr(schema_template.Coordinate, '_abstract') + assert hasattr(schema_template.Coordinate, "_abstract") def test_coordinate_attributes(): """Test that Coordinate has x and y attributes.""" coordinate = schema_template.Coordinate - assert 'x' in coordinate.__annotations__ - assert 'y' in coordinate.__annotations__ + assert "x" in coordinate.__annotations__ + assert "y" in coordinate.__annotations__ def test_location_multiple_inheritance(): """Test that Location inherits from both Address and Coordinate.""" - assert hasattr(schema_template, 'Location') + assert hasattr(schema_template, "Location") assert issubclass(schema_template.Location, schema_template.Address) assert issubclass(schema_template.Location, schema_template.Coordinate) @@ -110,41 +111,47 @@ def test_location_multiple_inheritance(): def test_location_attributes(): """Test that Location has its own attributes.""" location = schema_template.Location - assert 'name' in location.__annotations__ + assert "name" in location.__annotations__ def test_team_enum_exists(): """Test that Team enum is defined.""" - assert hasattr(schema_template, 'Team') + assert hasattr(schema_template, "Team") assert issubclass(schema_template.Team, EnumTemplate) def test_team_enum_values(): """Test that Team has expected enum values.""" team = schema_template.Team - assert hasattr(team, 'IT') - assert hasattr(team, 'Marketing') + assert hasattr(team, "IT") + assert hasattr(team, "Marketing") assert team.IT.value == "Information Technology" def test_contact_tagged_union_exists(): """Test that Contact tagged union is defined.""" - assert hasattr(schema_template, 'Contact') + assert hasattr(schema_template, "Contact") assert issubclass(schema_template.Contact, TaggedUnion) def test_contact_union_attributes(): """Test that Contact has union type attributes.""" contact = schema_template.Contact - assert 'local_number' in contact.__annotations__ - assert 'international' in contact.__annotations__ + assert "local_number" in contact.__annotations__ + assert "international" in contact.__annotations__ def test_all_classes_importable(): """Test that all template classes can be imported.""" classes = [ - 'Country', 'Address', 'Person', 'Employee', - 'Coordinate', 'Location', 'Team', 'Contact' + "Country", + "Address", + "Person", + "Employee", + "Coordinate", + "Location", + "Team", + "Contact", ] for cls_name in classes: assert hasattr(schema_template, cls_name), f"{cls_name} should be importable" @@ -154,6 +161,6 @@ def test_module_docstring(): """Test that module has docstring with metadata.""" doc = schema_template.__doc__ assert doc is not None - assert 'Title:' in doc - assert 'Description:' in doc - assert 'Authors:' in doc + assert "Title:" in doc + assert "Description:" in doc + assert "Authors:" in doc diff --git a/terminusdb_client/tests/test_scripts_main.py b/terminusdb_client/tests/test_scripts_main.py index b5e18ca6..95c6183c 100644 --- a/terminusdb_client/tests/test_scripts_main.py +++ b/terminusdb_client/tests/test_scripts_main.py @@ -1,4 +1,5 @@ """Tests for scripts/__main__.py module.""" + import sys from unittest.mock import patch, MagicMock @@ -6,22 +7,25 @@ def test_main_imports(): """Test that __main__ can be imported.""" from terminusdb_client.scripts import __main__ + assert __main__ is not None def test_main_has_tdbpy(): """Test that __main__ imports tdbpy function.""" from terminusdb_client.scripts import __main__ - assert hasattr(__main__, 'tdbpy') + + assert hasattr(__main__, "tdbpy") def test_main_execution(): """Test that __main__ calls tdbpy when executed.""" mock_tdbpy = MagicMock() - with patch('terminusdb_client.scripts.__main__.tdbpy', mock_tdbpy): + with patch("terminusdb_client.scripts.__main__.tdbpy", mock_tdbpy): # Simulate running as main module - with patch.object(sys, 'argv', ['__main__.py']): + with patch.object(sys, "argv", ["__main__.py"]): # Import and check the condition would trigger from terminusdb_client.scripts import __main__ - assert __main__.__name__ == 'terminusdb_client.scripts.__main__' + + assert __main__.__name__ == "terminusdb_client.scripts.__main__" diff --git a/terminusdb_client/tests/test_woqlQuery.py b/terminusdb_client/tests/test_woqlQuery.py index d281b130..7c6594d1 100644 --- a/terminusdb_client/tests/test_woqlQuery.py +++ b/terminusdb_client/tests/test_woqlQuery.py @@ -707,26 +707,59 @@ def test_dot(self): } def test_doc(self): - result = WOQLQuery().insert_document( - Doc({"@type": "Car", "wheels": 4}) - ) - assert result.to_dict() == {'@type': 'InsertDocument', 'document': {'@type': 'Value', 'dictionary': {'@type': 'DictionaryTemplate', 'data': [{'@type': 'FieldValuePair', 'field': '@type', 'value': {'@type': 'Value', 'data': {'@type': 'xsd:string', '@value': 'Car'}}}, {'@type': 'FieldValuePair', 'field': 'wheels', 'value': {'@type': 'Value', 'data': {'@type': 'xsd:integer', '@value': 4}}}]}}} + result = WOQLQuery().insert_document(Doc({"@type": "Car", "wheels": 4})) + assert result.to_dict() == { + "@type": "InsertDocument", + "document": { + "@type": "Value", + "dictionary": { + "@type": "DictionaryTemplate", + "data": [ + { + "@type": "FieldValuePair", + "field": "@type", + "value": { + "@type": "Value", + "data": {"@type": "xsd:string", "@value": "Car"}, + }, + }, + { + "@type": "FieldValuePair", + "field": "wheels", + "value": { + "@type": "Value", + "data": {"@type": "xsd:integer", "@value": 4}, + }, + }, + ], + }, + }, + } def test_var(self): - result = WOQLQuery().insert_document( - Doc({"@type": "Car", "wheels": Var("v")}) - ) + result = WOQLQuery().insert_document(Doc({"@type": "Car", "wheels": Var("v")})) assert result.to_dict() == { - '@type': 'InsertDocument', - 'document': {'@type': 'Value', - 'dictionary': {'@type': 'DictionaryTemplate', - 'data': [{'@type': 'FieldValuePair', - 'field': '@type', - 'value': {'@type': 'Value', - 'data': {'@type': 'xsd:string', - '@value': 'Car'}}}, - {'@type': 'FieldValuePair', - 'field': 'wheels', - 'value': {'@type': 'Value', - 'variable': 'v'}}]}}} + "@type": "InsertDocument", + "document": { + "@type": "Value", + "dictionary": { + "@type": "DictionaryTemplate", + "data": [ + { + "@type": "FieldValuePair", + "field": "@type", + "value": { + "@type": "Value", + "data": {"@type": "xsd:string", "@value": "Car"}, + }, + }, + { + "@type": "FieldValuePair", + "field": "wheels", + "value": {"@type": "Value", "variable": "v"}, + }, + ], + }, + }, + } diff --git a/terminusdb_client/tests/test_woql_idgen_random.py b/terminusdb_client/tests/test_woql_idgen_random.py index bb11d169..419abe20 100644 --- a/terminusdb_client/tests/test_woql_idgen_random.py +++ b/terminusdb_client/tests/test_woql_idgen_random.py @@ -1,4 +1,5 @@ """Unit tests for idgen_random WOQL method""" + from terminusdb_client.woqlquery.woql_query import WOQLQuery @@ -27,9 +28,11 @@ def test_idgen_random_with_prefix(self): def test_idgen_random_chaining(self): """Test idgen_random can be chained with other operations""" - woql = (WOQLQuery() - .triple("v:Person", "rdf:type", "@schema:Person") - .idgen_random("Person/", "v:PersonID")) + woql = ( + WOQLQuery() + .triple("v:Person", "rdf:type", "@schema:Person") + .idgen_random("Person/", "v:PersonID") + ) result = woql.to_dict() assert result["@type"] == "And" @@ -39,9 +42,11 @@ def test_idgen_random_chaining(self): def test_idgen_random_multiple_calls(self): """Test multiple idgen_random calls in same query""" - woql = (WOQLQuery() - .idgen_random("Person/", "v:PersonID") - .idgen_random("Order/", "v:OrderID")) + woql = ( + WOQLQuery() + .idgen_random("Person/", "v:PersonID") + .idgen_random("Order/", "v:OrderID") + ) result = woql.to_dict() assert result["@type"] == "And" @@ -73,10 +78,12 @@ def test_idgen_random_variable_output(self): def test_idgen_random_in_query_chain(self): """Test idgen_random in complex query chain""" - woql = (WOQLQuery() - .triple("v:Person", "rdf:type", "@schema:Person") - .idgen_random("Person/", "v:PersonID") - .triple("v:PersonID", "@schema:name", "v:Name")) + woql = ( + WOQLQuery() + .triple("v:Person", "rdf:type", "@schema:Person") + .idgen_random("Person/", "v:PersonID") + .triple("v:PersonID", "@schema:name", "v:Name") + ) result = woql.to_dict() assert result["@type"] == "And" diff --git a/terminusdb_client/tests/test_woql_localize.py b/terminusdb_client/tests/test_woql_localize.py new file mode 100644 index 00000000..5b068cad --- /dev/null +++ b/terminusdb_client/tests/test_woql_localize.py @@ -0,0 +1,246 @@ +""" +Unit tests for WOQL localize() - verifying JSON structure only. +These tests do NOT connect to a database - they only verify the generated WOQL JSON. + +These tests align with the JavaScript client's woqlLocalize.spec.js tests. +""" + +from terminusdb_client.woqlquery import ( + WOQLQuery, + Var, + VarsUnique, + _reset_unique_var_counter, +) + + +class TestWOQLLocalize: + """Test suite for WOQL localize() method.""" + + def setup_method(self): + """Reset unique var counter before each test for predictable names.""" + _reset_unique_var_counter(0) + + def test_hide_local_variables_from_outer_scope(self): + """Should hide local variables from outer scope (basic test).""" + (localized, v) = WOQLQuery().localize( + { + "local_only": None, + } + ) + query = WOQLQuery().woql_and( + WOQLQuery().triple("v:x", "v:y", "v:z"), + localized( + WOQLQuery().triple(v.local_only, "knows", "v:someone"), + ), + ) + + result = query.to_dict() + + # Check that query has proper structure + assert result["@type"] == "And" + assert isinstance(result["and"], list) + # Second element should be Select + assert result["and"][1]["@type"] == "Select" + assert isinstance(result["and"][1]["variables"], list) + + # CRITICAL: select("") creates variables:[] to hide all local variables + assert result["and"][1]["variables"] == [] + + def test_bind_outer_parameters_via_eq(self): + """Should bind outer parameters via eq() clauses.""" + (localized, v) = WOQLQuery().localize( + { + "param1": "v:input1", + "param2": "v:input2", + "local": None, + } + ) + + query = localized( + WOQLQuery().triple(v.param1, "knows", v.param2), + ) + + result = query.to_dict() + + # Structure: And with eq bindings + Select wrapper + assert result["@type"] == "And" + and_clauses = result["and"] + + # Should have eq bindings before the select + eq_count = sum(1 for c in and_clauses if c.get("@type") == "Equals") + assert eq_count >= 2 # At least 2 eq bindings for 2 outer params + + # Last element should be Select + select_clauses = [c for c in and_clauses if c.get("@type") == "Select"] + assert len(select_clauses) >= 1 + + def test_functional_mode(self): + """Should work in functional mode.""" + (localized, v) = WOQLQuery().localize( + { + "x": "v:external_x", + "temp": None, + } + ) + + query = localized( + WOQLQuery().woql_and( + WOQLQuery().eq( + v.temp, {"@type": "xsd:string", "@value": "intermediate"} + ), + WOQLQuery().triple(v.x, "knows", v.temp), + ), + ) + + result = query.to_dict() + + # Structure should be And with eq + Select + assert result["@type"] == "And" + and_clauses = result["and"] + + # Should have eq binding for external_x + eq_count = sum(1 for c in and_clauses if c.get("@type") == "Equals") + assert eq_count >= 1 + + # Should have Select with empty variables + select_clauses = [c for c in and_clauses if c.get("@type") == "Select"] + assert len(select_clauses) >= 1 + assert select_clauses[-1]["variables"] == [] + + def test_generate_unique_variable_names(self): + """Should generate unique variable names.""" + _reset_unique_var_counter(0) + + (localized1, v1) = WOQLQuery().localize( + { + "var1": None, + } + ) + + (localized2, v2) = WOQLQuery().localize( + { + "var1": None, + } + ) + + # Variable names should be different between calls + assert v1.var1.name != v2.var1.name + + def test_handle_only_local_variables(self): + """Should handle only local variables (no outer bindings).""" + (localized, v) = WOQLQuery().localize( + { + "local1": None, + "local2": None, + } + ) + + query = localized( + WOQLQuery().triple(v.local1, "knows", v.local2), + ) + + result = query.to_dict() + + # With no outer bindings, should be just Select + assert result["@type"] == "Select" + assert result["variables"] == [] + + # Query should be directly the triple (wrapped in And from select) + assert result["query"]["@type"] in ["Triple", "And"] + + def test_handle_only_outer_parameters(self): + """Should handle only outer parameters (no local variables).""" + (localized, v) = WOQLQuery().localize( + { + "param1": "v:input1", + "param2": "v:input2", + } + ) + + query = localized( + WOQLQuery().triple(v.param1, "knows", v.param2), + ) + + result = query.to_dict() + assert result["@type"] == "And" + # Should have eq bindings + Select + assert len(result["and"]) >= 3 + + def test_preserve_variable_types(self): + """Should preserve variable types (Var instances).""" + (localized, v) = WOQLQuery().localize( + { + "input": "v:x", + "local": None, + } + ) + + # v.input and v.local should be Var instances + assert isinstance(v.input, Var) + assert isinstance(v.local, Var) + + def test_handle_empty_parameter_specification(self): + """Should handle empty parameter specification.""" + (localized, v) = WOQLQuery().localize({}) + + query = localized( + WOQLQuery().triple("v:s", "v:p", "v:o"), + ) + + result = query.to_dict() + assert result["@type"] == "Select" + assert result["variables"] == [] + + # No eq bindings, just the query + assert "query" in result + + +class TestVarsUnique: + """Test suite for VarsUnique class.""" + + def setup_method(self): + """Reset unique var counter before each test.""" + _reset_unique_var_counter(0) + + def test_generates_unique_names(self): + """VarsUnique should generate unique variable names.""" + v1 = VarsUnique("x", "y") + v2 = VarsUnique("x", "y") + + # Names should have counter suffix + assert "_" in v1.x.name + assert "_" in v1.y.name + + # Different calls should have different names + assert v1.x.name != v2.x.name + assert v1.y.name != v2.y.name + + def test_counter_increments(self): + """Counter should increment for each variable.""" + _reset_unique_var_counter(0) + v = VarsUnique("a", "b", "c") + + # Each variable should have incrementing suffix + assert v.a.name == "a_1" + assert v.b.name == "b_2" + assert v.c.name == "c_3" + + def test_reset_counter(self): + """Counter reset should work correctly.""" + _reset_unique_var_counter(100) + v = VarsUnique("test") + assert v.test.name == "test_101" + + def test_var_instances(self): + """VarsUnique should create Var instances.""" + v = VarsUnique("foo", "bar") + assert isinstance(v.foo, Var) + assert isinstance(v.bar, Var) + + def test_to_dict(self): + """Var.to_dict should return proper WOQL structure.""" + _reset_unique_var_counter(0) + v = VarsUnique("myvar") + result = v.myvar.to_dict() + + assert result == {"@type": "Value", "variable": "myvar_1"} diff --git a/terminusdb_client/tests/test_woql_query_advanced.py b/terminusdb_client/tests/test_woql_query_advanced.py index 8aa8c526..c1a5d33c 100644 --- a/terminusdb_client/tests/test_woql_query_advanced.py +++ b/terminusdb_client/tests/test_woql_query_advanced.py @@ -1,10 +1,12 @@ """Tests for advanced WOQL query methods using standardized helpers.""" + from terminusdb_client.woqlquery.woql_query import WOQLQuery from terminusdb_client.tests.woql_test_helpers import WOQLTestHelpers as H # Optional Query Tests + def test_opt_basic(): """Test opt() creates proper Optional structure.""" query = WOQLQuery().opt() @@ -31,6 +33,7 @@ def test_opt_args_introspection(): # Result Control Tests + def test_limit_basic(): """Test limit() creates proper Limit structure.""" query = WOQLQuery().limit(10) @@ -136,6 +139,7 @@ def test_order_by_args_introspection(): # Document Operations Tests + def test_insert_document_basic(): """Test insert_document() creates proper InsertDocument structure.""" query = WOQLQuery().insert_document({"@type": "Person", "name": "Alice"}) @@ -170,7 +174,9 @@ def test_insert_document_args_introspection(): def test_update_document_basic(): """Test update_document() creates proper UpdateDocument structure.""" - query = WOQLQuery().update_document({"@id": "Person/Alice", "name": "Alice Updated"}) + query = WOQLQuery().update_document( + {"@id": "Person/Alice", "name": "Alice Updated"} + ) H.assert_query_type(query, "UpdateDocument") H.assert_has_key(query, "document") @@ -251,6 +257,7 @@ def test_read_document_args_introspection(): # Substring Test + def test_substr_basic(): """Test substr() creates proper Substring structure.""" query = WOQLQuery().substr("test string", 4, "v:Substr", before=0, after=6) @@ -290,6 +297,7 @@ def test_substr_args_introspection(): # Once Test + def test_once_basic(): """Test once() creates proper Once structure.""" query = WOQLQuery().once() @@ -316,6 +324,7 @@ def test_once_args_introspection(): # woql_not Test + def test_woql_not_basic(): """Test woql_not() creates proper Not structure.""" query = WOQLQuery().woql_not() @@ -342,6 +351,7 @@ def test_woql_not_args_introspection(): # Chaining Tests + def test_chaining_limit_and_order(): """Test chaining limit() with order_by().""" query = WOQLQuery().triple("v:X", "rdf:type", "Person") @@ -374,6 +384,7 @@ def test_pagination_pattern(): # Update Operations Chaining + def test_insert_then_read(): """Test chaining insert_document() and read_document().""" query = WOQLQuery().insert_document({"@type": "Person"}, "v:ID") diff --git a/terminusdb_client/tests/test_woql_query_basics.py b/terminusdb_client/tests/test_woql_query_basics.py index da4e9e5b..af47a35a 100644 --- a/terminusdb_client/tests/test_woql_query_basics.py +++ b/terminusdb_client/tests/test_woql_query_basics.py @@ -1,4 +1,5 @@ """Tests for basic woql_query.py classes and functions.""" + from terminusdb_client.woqlquery.woql_query import Var, Vars, Doc, SHORT_NAME_MAPPING @@ -54,7 +55,7 @@ def test_doc_with_string(): assert result == { "@type": "Value", - "data": {"@type": "xsd:string", "@value": "test string"} + "data": {"@type": "xsd:string", "@value": "test string"}, } @@ -65,11 +66,11 @@ def test_doc_with_bool(): assert doc_true.to_dict() == { "@type": "Value", - "data": {"@type": "xsd:boolean", "@value": True} + "data": {"@type": "xsd:boolean", "@value": True}, } assert doc_false.to_dict() == { "@type": "Value", - "data": {"@type": "xsd:boolean", "@value": False} + "data": {"@type": "xsd:boolean", "@value": False}, } @@ -79,10 +80,7 @@ def test_doc_with_int(): result = doc.to_dict() - assert result == { - "@type": "Value", - "data": {"@type": "xsd:integer", "@value": 42} - } + assert result == {"@type": "Value", "data": {"@type": "xsd:integer", "@value": 42}} def test_doc_with_float(): @@ -93,7 +91,7 @@ def test_doc_with_float(): assert result == { "@type": "Value", - "data": {"@type": "xsd:decimal", "@value": 3.14} + "data": {"@type": "xsd:decimal", "@value": 3.14}, } @@ -118,7 +116,7 @@ def test_doc_with_list(): # Check first element assert result["list"][0] == { "@type": "Value", - "data": {"@type": "xsd:integer", "@value": 1} + "data": {"@type": "xsd:integer", "@value": 1}, } @@ -129,10 +127,7 @@ def test_doc_with_var(): result = doc.to_dict() - assert result == { - "@type": "Value", - "variable": "myVar" - } + assert result == {"@type": "Value", "variable": "myVar"} def test_doc_with_dict(): @@ -150,7 +145,7 @@ def test_doc_with_dict(): name_pair = next(p for p in result["dictionary"]["data"] if p["field"] == "name") assert name_pair["value"] == { "@type": "Value", - "data": {"@type": "xsd:string", "@value": "Alice"} + "data": {"@type": "xsd:string", "@value": "Alice"}, } @@ -165,10 +160,7 @@ def test_doc_str(): def test_doc_nested_structures(): """Test Doc with nested list and dict.""" - doc = Doc({ - "list": [1, 2, 3], - "nested": {"inner": "value"} - }) + doc = Doc({"list": [1, 2, 3], "nested": {"inner": "value"}}) result = doc.to_dict() @@ -208,7 +200,7 @@ def test_doc_with_empty_dict(): assert result == { "@type": "Value", - "dictionary": {"@type": "DictionaryTemplate", "data": []} + "dictionary": {"@type": "DictionaryTemplate", "data": []}, } @@ -225,15 +217,17 @@ def test_doc_with_nested_var(): def test_doc_with_mixed_types(): """Test Doc with various mixed types in dict.""" var = Var("status") - doc = Doc({ - "id": 1, - "name": "Test", - "active": True, - "score": 95.5, - "status": var, - "tags": ["a", "b"], - "metadata": None - }) + doc = Doc( + { + "id": 1, + "name": "Test", + "active": True, + "score": 95.5, + "status": var, + "tags": ["a", "b"], + "metadata": None, + } + ) result = doc.to_dict() diff --git a/terminusdb_client/tests/test_woql_query_builders.py b/terminusdb_client/tests/test_woql_query_builders.py index 2df6301f..b599c563 100644 --- a/terminusdb_client/tests/test_woql_query_builders.py +++ b/terminusdb_client/tests/test_woql_query_builders.py @@ -1,10 +1,12 @@ """Tests for WOQL query builder methods using standardized helpers.""" + from terminusdb_client.woqlquery.woql_query import WOQLQuery, Var from terminusdb_client.tests.woql_test_helpers import WOQLTestHelpers as H # Basic Query Builders + def test_triple_basic(): """Test triple() creates proper Triple structure.""" query = WOQLQuery().triple("v:Subject", "rdf:type", "v:Object") @@ -287,6 +289,7 @@ def test_execute_with_commit(): # Chaining Tests + def test_chaining_triple_and_triple(): """Test chaining multiple triple() calls.""" query = WOQLQuery().triple("v:X", "rdf:type", "Person") @@ -320,6 +323,7 @@ def test_operators_return_self(): # Edge Cases + def test_select_with_empty_list(): """Test select() explicitly with empty list.""" query = WOQLQuery().select() @@ -365,6 +369,7 @@ def test_triple_with_string_object(): # Args Introspection Tests + def test_using_args_introspection(): """Test using() returns args list when called with 'args'.""" result = WOQLQuery().using("args") diff --git a/terminusdb_client/tests/test_woql_query_cleaners.py b/terminusdb_client/tests/test_woql_query_cleaners.py index 64466aef..f6c41e51 100644 --- a/terminusdb_client/tests/test_woql_query_cleaners.py +++ b/terminusdb_client/tests/test_woql_query_cleaners.py @@ -1,4 +1,5 @@ """Tests for WOQLQuery cleaning and expansion methods.""" + import datetime from terminusdb_client.woqlquery.woql_query import WOQLQuery, Var, Doc diff --git a/terminusdb_client/tests/test_woql_query_methods.py b/terminusdb_client/tests/test_woql_query_methods.py index 26f6c3fd..00741790 100644 --- a/terminusdb_client/tests/test_woql_query_methods.py +++ b/terminusdb_client/tests/test_woql_query_methods.py @@ -1,4 +1,5 @@ """Tests for WOQLQuery class methods in woql_query.py.""" + from terminusdb_client.woqlquery.woql_query import WOQLQuery, Var @@ -34,17 +35,17 @@ def test_woqlquery_aliases(): query = WOQLQuery() # Check aliases exist - assert hasattr(query, 'subsumption') - assert hasattr(query, 'equals') - assert hasattr(query, 'substring') - assert hasattr(query, 'update') - assert hasattr(query, 'delete') - assert hasattr(query, 'read') - assert hasattr(query, 'insert') - assert hasattr(query, 'optional') - assert hasattr(query, 'idgenerator') - assert hasattr(query, 'concatenate') - assert hasattr(query, 'typecast') + assert hasattr(query, "subsumption") + assert hasattr(query, "equals") + assert hasattr(query, "substring") + assert hasattr(query, "update") + assert hasattr(query, "delete") + assert hasattr(query, "read") + assert hasattr(query, "insert") + assert hasattr(query, "optional") + assert hasattr(query, "idgenerator") + assert hasattr(query, "concatenate") + assert hasattr(query, "typecast") def test_woqlquery_add_operator(): @@ -131,46 +132,32 @@ def test_contains_update_check_true_direct(): def test_contains_update_check_in_consequent(): """Test _contains_update_check detects updates in consequent.""" - query = WOQLQuery({ - "@type": "When", - "consequent": {"@type": "DeleteTriple"} - }) + query = WOQLQuery({"@type": "When", "consequent": {"@type": "DeleteTriple"}}) assert query._contains_update_check() is True def test_contains_update_check_in_nested_query(): """Test _contains_update_check detects updates in nested query.""" - query = WOQLQuery({ - "@type": "Select", - "query": {"@type": "UpdateObject"} - }) + query = WOQLQuery({"@type": "Select", "query": {"@type": "UpdateObject"}}) assert query._contains_update_check() is True def test_contains_update_check_in_and(): """Test _contains_update_check detects updates in And clause.""" - query = WOQLQuery({ - "@type": "And", - "and": [ - {"@type": "Triple"}, - {"@type": "AddQuad"} - ] - }) + query = WOQLQuery( + {"@type": "And", "and": [{"@type": "Triple"}, {"@type": "AddQuad"}]} + ) assert query._contains_update_check() is True def test_contains_update_check_in_or(): """Test _contains_update_check detects updates in Or clause.""" - query = WOQLQuery({ - "@type": "Or", - "or": [ - {"@type": "Triple"}, - {"@type": "DeleteObject"} - ] - }) + query = WOQLQuery( + {"@type": "Or", "or": [{"@type": "Triple"}, {"@type": "DeleteObject"}]} + ) assert query._contains_update_check() is True diff --git a/terminusdb_client/tests/test_woql_query_triples_quads.py b/terminusdb_client/tests/test_woql_query_triples_quads.py index b672c6c8..d9b13a10 100644 --- a/terminusdb_client/tests/test_woql_query_triples_quads.py +++ b/terminusdb_client/tests/test_woql_query_triples_quads.py @@ -1,4 +1,5 @@ """Tests for WOQL triple and quad methods using standardized helpers.""" + import datetime from terminusdb_client.woqlquery.woql_query import WOQLQuery from terminusdb_client.tests.woql_test_helpers import WOQLTestHelpers as H @@ -6,6 +7,7 @@ # Added/Removed Triple Tests + def test_added_triple_basic(): """Test added_triple() creates proper AddedTriple structure.""" query = WOQLQuery().added_triple("v:S", "v:P", "v:O") @@ -45,6 +47,7 @@ def test_removed_triple_with_strings(): # Quad Tests + def test_quad_basic(): """Test quad() creates proper Triple with graph.""" query = WOQLQuery().quad("v:S", "v:P", "v:O", "instance/main") @@ -146,6 +149,7 @@ def test_removed_quad_invalid_graph(): # Type Conversion Helper Tests + def test_string_helper(): """Test string() helper creates proper xsd:string.""" query = WOQLQuery() @@ -272,6 +276,7 @@ def test_iri_helper(): # Subsumption and Equality Tests + def test_sub_basic(): """Test sub() creates proper Subsumption structure.""" query = WOQLQuery().sub("owl:Thing", "Person") @@ -368,6 +373,7 @@ def test_eq_args_introspection(): # Chaining Tests + def test_chaining_added_triple(): """Test chaining added_triple() calls.""" query = WOQLQuery().added_triple("v:S1", "v:P1", "v:O1") diff --git a/terminusdb_client/tests/test_woql_query_utils.py b/terminusdb_client/tests/test_woql_query_utils.py index d4f1c66e..3fefa75b 100644 --- a/terminusdb_client/tests/test_woql_query_utils.py +++ b/terminusdb_client/tests/test_woql_query_utils.py @@ -1,4 +1,5 @@ """Tests for WOQLQuery utility methods.""" + from terminusdb_client.woqlquery.woql_query import WOQLQuery @@ -39,13 +40,7 @@ def test_find_last_subject_in_and(): """Test _find_last_subject finds subject in And clause.""" query = WOQLQuery() triple = {"@type": "Triple", "subject": "v:Y"} - json_obj = { - "@type": "And", - "and": [ - {"@type": "Other"}, - triple - ] - } + json_obj = {"@type": "And", "and": [{"@type": "Other"}, triple]} result = query._find_last_subject(json_obj) @@ -56,13 +51,7 @@ def test_find_last_subject_in_or(): """Test _find_last_subject finds subject in Or clause.""" query = WOQLQuery() triple = {"@type": "Triple", "subject": "v:Z"} - json_obj = { - "@type": "Or", - "or": [ - {"@type": "Other"}, - triple - ] - } + json_obj = {"@type": "Or", "or": [{"@type": "Other"}, triple]} result = query._find_last_subject(json_obj) @@ -73,10 +62,7 @@ def test_find_last_subject_in_nested_query(): """Test _find_last_subject finds subject in nested query.""" query = WOQLQuery() triple = {"@type": "Triple", "subject": "v:A"} - json_obj = { - "@type": "Select", - "query": triple - } + json_obj = {"@type": "Select", "query": triple} result = query._find_last_subject(json_obj) @@ -98,10 +84,7 @@ def test_find_last_subject_reverse_order(): query = WOQLQuery() first = {"@type": "Triple", "subject": "v:First"} last = {"@type": "Triple", "subject": "v:Last"} - json_obj = { - "@type": "And", - "and": [first, last] - } + json_obj = {"@type": "And", "and": [first, last]} result = query._find_last_subject(json_obj) @@ -116,7 +99,7 @@ def test_find_last_property_with_object_property(): "@type": "Triple", "subject": "v:X", "predicate": "rdf:type", - "object": "owl:ObjectProperty" + "object": "owl:ObjectProperty", } result = query._find_last_property(json_obj) @@ -131,7 +114,7 @@ def test_find_last_property_with_datatype_property(): "@type": "Triple", "subject": "v:X", "predicate": "rdf:type", - "object": "owl:DatatypeProperty" + "object": "owl:DatatypeProperty", } result = query._find_last_property(json_obj) @@ -146,7 +129,7 @@ def test_find_last_property_with_domain(): "@type": "Triple", "subject": "v:X", "predicate": "rdfs:domain", - "object": "v:Y" + "object": "v:Y", } result = query._find_last_property(json_obj) @@ -161,7 +144,7 @@ def test_find_last_property_with_range(): "@type": "Triple", "subject": "v:X", "predicate": "rdfs:range", - "object": "xsd:string" + "object": "xsd:string", } result = query._find_last_property(json_obj) @@ -176,15 +159,9 @@ def test_find_last_property_in_and(): "@type": "Triple", "subject": "v:X", "predicate": "rdfs:domain", - "object": "v:Y" - } - json_obj = { - "@type": "And", - "and": [ - {"@type": "Other"}, - prop_triple - ] + "object": "v:Y", } + json_obj = {"@type": "And", "and": [{"@type": "Other"}, prop_triple]} result = query._find_last_property(json_obj) @@ -194,18 +171,8 @@ def test_find_last_property_in_and(): def test_find_last_property_in_or(): """Test _find_last_property finds property in Or clause.""" query = WOQLQuery() - prop_triple = { - "@type": "Triple", - "subject": "v:X", - "object": "owl:ObjectProperty" - } - json_obj = { - "@type": "Or", - "or": [ - {"@type": "Other"}, - prop_triple - ] - } + prop_triple = {"@type": "Triple", "subject": "v:X", "object": "owl:ObjectProperty"} + json_obj = {"@type": "Or", "or": [{"@type": "Other"}, prop_triple]} result = query._find_last_property(json_obj) @@ -219,12 +186,9 @@ def test_find_last_property_in_nested_query(): "@type": "Triple", "subject": "v:X", "predicate": "rdfs:range", - "object": "v:Y" - } - json_obj = { - "@type": "Select", - "query": prop_triple + "object": "v:Y", } + json_obj = {"@type": "Select", "query": prop_triple} result = query._find_last_property(json_obj) @@ -238,7 +202,7 @@ def test_find_last_property_not_found(): "@type": "Triple", "subject": "v:X", "predicate": "someOther:predicate", - "object": "v:Y" + "object": "v:Y", } result = query._find_last_property(json_obj) @@ -340,14 +304,7 @@ def test_find_last_subject_deeply_nested(): triple = {"@type": "Triple", "subject": "v:Deep"} json_obj = { "@type": "And", - "and": [ - { - "@type": "Or", - "or": [ - {"@type": "Select", "query": triple} - ] - } - ] + "and": [{"@type": "Or", "or": [{"@type": "Select", "query": triple}]}], } result = query._find_last_subject(json_obj) @@ -362,18 +319,11 @@ def test_find_last_property_deeply_nested(): "@type": "Triple", "subject": "v:X", "predicate": "rdfs:domain", - "object": "v:Y" + "object": "v:Y", } json_obj = { "@type": "And", - "and": [ - { - "@type": "Or", - "or": [ - {"@type": "Select", "query": triple} - ] - } - ] + "and": [{"@type": "Or", "or": [{"@type": "Select", "query": triple}]}], } result = query._find_last_property(json_obj) diff --git a/terminusdb_client/tests/test_woql_rdflist.py b/terminusdb_client/tests/test_woql_rdflist.py new file mode 100644 index 00000000..fe89c152 --- /dev/null +++ b/terminusdb_client/tests/test_woql_rdflist.py @@ -0,0 +1,440 @@ +""" +Unit tests for WOQL RDFList library operations - verifying JSON structure. +These tests do NOT connect to a database - they only verify the generated WOQL JSON. + +These tests align with the JavaScript client's RDFList implementation. +""" + +import pytest +from terminusdb_client.woqlquery import ( + WOQLQuery, + WOQLLibrary, + _reset_unique_var_counter, +) + + +class TestWOQLLibraryRDFList: + """Test suite for WOQLLibrary RDFList operations.""" + + def setup_method(self): + """Reset unique var counter before each test for predictable names.""" + _reset_unique_var_counter(0) + + def test_lib_method_returns_library(self): + """WOQLQuery().lib() should return a WOQLLibrary instance.""" + lib = WOQLQuery().lib() + assert isinstance(lib, WOQLLibrary) + + def test_rdflist_peek_structure(self): + """rdflist_peek should generate correct query structure.""" + query = WOQLQuery().lib().rdflist_peek("v:list_head", "v:first_value") + result = query.to_dict() + + # Should be an And with eq bindings and Select + assert result["@type"] == "And" + + # Should contain a Select to scope local variables + has_select = any( + item.get("@type") == "Select" for item in result.get("and", []) + ) + assert has_select + + def test_rdflist_length_structure(self): + """rdflist_length should generate correct query structure.""" + query = WOQLQuery().lib().rdflist_length("v:list_head", "v:count") + result = query.to_dict() + + assert result["@type"] == "And" + + def test_rdflist_member_structure(self): + """rdflist_member should generate correct query structure.""" + query = WOQLQuery().lib().rdflist_member("v:list_head", "v:value") + result = query.to_dict() + + assert result["@type"] == "And" + + def test_rdflist_list_structure(self): + """rdflist_list should generate group_by query structure.""" + query = WOQLQuery().lib().rdflist_list("v:list_head", "v:all_values") + result = query.to_dict() + + # Should contain a group_by somewhere in the structure + def contains_group_by(obj): + if isinstance(obj, dict): + if obj.get("@type") == "GroupBy": + return True + for v in obj.values(): + if contains_group_by(v): + return True + elif isinstance(obj, list): + for item in obj: + if contains_group_by(item): + return True + return False + + assert contains_group_by(result) + + def test_rdflist_last_structure(self): + """rdflist_last should generate path query to find last element.""" + query = WOQLQuery().lib().rdflist_last("v:list_head", "v:last_value") + result = query.to_dict() + + # Should contain a Path query + def contains_path(obj): + if isinstance(obj, dict): + if obj.get("@type") == "Path": + return True + for v in obj.values(): + if contains_path(v): + return True + elif isinstance(obj, list): + for item in obj: + if contains_path(item): + return True + return False + + assert contains_path(result) + + def test_rdflist_nth0_static_index(self): + """rdflist_nth0 with static index should work.""" + query = WOQLQuery().lib().rdflist_nth0("v:list_head", 2, "v:value") + result = query.to_dict() + + # Should be valid query structure + assert result["@type"] == "And" + + def test_rdflist_nth0_index_zero(self): + """rdflist_nth0 with index 0 should use peek logic.""" + query = WOQLQuery().lib().rdflist_nth0("v:list_head", 0, "v:value") + result = query.to_dict() + + assert result["@type"] == "And" + + def test_rdflist_nth0_negative_index_raises(self): + """rdflist_nth0 with negative index should raise ValueError.""" + with pytest.raises(ValueError, match="index >= 0"): + WOQLQuery().lib().rdflist_nth0("v:list", -1, "v:val") + + def test_rdflist_nth1_static_index(self): + """rdflist_nth1 with static index should work.""" + query = WOQLQuery().lib().rdflist_nth1("v:list_head", 1, "v:value") + result = query.to_dict() + + assert result["@type"] == "And" + + def test_rdflist_nth1_zero_index_raises(self): + """rdflist_nth1 with index < 1 should raise ValueError.""" + with pytest.raises(ValueError, match="index >= 1"): + WOQLQuery().lib().rdflist_nth1("v:list", 0, "v:val") + + def test_rdflist_pop_structure(self): + """rdflist_pop should generate delete and add triple operations.""" + query = WOQLQuery().lib().rdflist_pop("v:list_head", "v:popped") + result = query.to_dict() + + # Should contain DeleteTriple and AddTriple + def contains_type(obj, type_name): + if isinstance(obj, dict): + if obj.get("@type") == type_name: + return True + for v in obj.values(): + if contains_type(v, type_name): + return True + elif isinstance(obj, list): + for item in obj: + if contains_type(item, type_name): + return True + return False + + assert contains_type(result, "DeleteTriple") + assert contains_type(result, "AddTriple") + + def test_rdflist_push_structure(self): + """rdflist_push should generate delete and add triple operations.""" + query = WOQLQuery().lib().rdflist_push("v:list_head", "v:new_value") + result = query.to_dict() + + def contains_type(obj, type_name): + if isinstance(obj, dict): + if obj.get("@type") == type_name: + return True + for v in obj.values(): + if contains_type(v, type_name): + return True + elif isinstance(obj, list): + for item in obj: + if contains_type(item, type_name): + return True + return False + + assert contains_type(result, "DeleteTriple") + assert contains_type(result, "AddTriple") + assert contains_type(result, "RandomKey") # Python client uses RandomKey + + def test_rdflist_append_structure(self): + """rdflist_append should generate append query structure.""" + query = WOQLQuery().lib().rdflist_append("v:list_head", "v:new_value") + result = query.to_dict() + + def contains_type(obj, type_name): + if isinstance(obj, dict): + if obj.get("@type") == type_name: + return True + for v in obj.values(): + if contains_type(v, type_name): + return True + elif isinstance(obj, list): + for item in obj: + if contains_type(item, type_name): + return True + return False + + assert contains_type(result, "Path") + assert contains_type(result, "AddTriple") + + def test_rdflist_clear_structure(self): + """rdflist_clear should generate delete operations.""" + query = WOQLQuery().lib().rdflist_clear("v:list_head", "v:empty_list") + result = query.to_dict() + + def contains_type(obj, type_name): + if isinstance(obj, dict): + if obj.get("@type") == type_name: + return True + for v in obj.values(): + if contains_type(v, type_name): + return True + elif isinstance(obj, list): + for item in obj: + if contains_type(item, type_name): + return True + return False + + assert contains_type(result, "DeleteTriple") + assert contains_type(result, "Equals") + + def test_rdflist_empty_structure(self): + """rdflist_empty should return eq with rdf:nil.""" + query = WOQLQuery().lib().rdflist_empty("v:empty") + result = query.to_dict() + + assert result["@type"] == "Equals" + + def test_rdflist_is_empty_structure(self): + """rdflist_is_empty should return eq check for rdf:nil.""" + query = WOQLQuery().lib().rdflist_is_empty("v:list") + result = query.to_dict() + + assert result["@type"] == "Equals" + + def test_rdflist_slice_structure(self): + """rdflist_slice should generate group_by with path pattern.""" + query = WOQLQuery().lib().rdflist_slice("v:list_head", 0, 3, "v:result") + result = query.to_dict() + + def contains_group_by(obj): + if isinstance(obj, dict): + if obj.get("@type") == "GroupBy": + return True + for v in obj.values(): + if contains_group_by(v): + return True + elif isinstance(obj, list): + for item in obj: + if contains_group_by(item): + return True + return False + + assert contains_group_by(result) + + def test_rdflist_slice_empty_range(self): + """rdflist_slice with start >= end should return empty list eq.""" + query = WOQLQuery().lib().rdflist_slice("v:list", 5, 3, "v:result") + result = query.to_dict() + + # Should contain Equals for empty list + def contains_equals(obj): + if isinstance(obj, dict): + if obj.get("@type") == "Equals": + return True + for v in obj.values(): + if contains_equals(v): + return True + elif isinstance(obj, list): + for item in obj: + if contains_equals(item): + return True + return False + + assert contains_equals(result) + + def test_rdflist_slice_negative_indices_raises(self): + """rdflist_slice with negative indices should raise ValueError.""" + with pytest.raises(ValueError, match="negative indices"): + WOQLQuery().lib().rdflist_slice("v:list", -1, 3, "v:result") + + def test_rdflist_insert_at_zero(self): + """rdflist_insert at position 0 should work.""" + query = WOQLQuery().lib().rdflist_insert("v:list_head", 0, "v:value") + result = query.to_dict() + + def contains_type(obj, type_name): + if isinstance(obj, dict): + if obj.get("@type") == type_name: + return True + for v in obj.values(): + if contains_type(v, type_name): + return True + elif isinstance(obj, list): + for item in obj: + if contains_type(item, type_name): + return True + return False + + assert contains_type(result, "AddTriple") + assert contains_type(result, "DeleteTriple") + + def test_rdflist_insert_at_position(self): + """rdflist_insert at position > 0 should work.""" + query = WOQLQuery().lib().rdflist_insert("v:list_head", 2, "v:value") + result = query.to_dict() + + def contains_path(obj): + if isinstance(obj, dict): + if obj.get("@type") == "Path": + return True + for v in obj.values(): + if contains_path(v): + return True + elif isinstance(obj, list): + for item in obj: + if contains_path(item): + return True + return False + + assert contains_path(result) + + def test_rdflist_insert_negative_position_raises(self): + """rdflist_insert with negative position should raise ValueError.""" + with pytest.raises(ValueError, match="position >= 0"): + WOQLQuery().lib().rdflist_insert("v:list", -1, "v:val") + + def test_rdflist_drop_at_zero(self): + """rdflist_drop at position 0 should work.""" + query = WOQLQuery().lib().rdflist_drop("v:list_head", 0) + result = query.to_dict() + + def contains_type(obj, type_name): + if isinstance(obj, dict): + if obj.get("@type") == type_name: + return True + for v in obj.values(): + if contains_type(v, type_name): + return True + elif isinstance(obj, list): + for item in obj: + if contains_type(item, type_name): + return True + return False + + assert contains_type(result, "DeleteTriple") + assert contains_type(result, "AddTriple") + + def test_rdflist_drop_negative_position_raises(self): + """rdflist_drop with negative position should raise ValueError.""" + with pytest.raises(ValueError, match="position >= 0"): + WOQLQuery().lib().rdflist_drop("v:list", -1) + + def test_rdflist_swap_structure(self): + """rdflist_swap should generate swap operations.""" + query = WOQLQuery().lib().rdflist_swap("v:list_head", 0, 2) + result = query.to_dict() + + def contains_type(obj, type_name): + if isinstance(obj, dict): + if obj.get("@type") == type_name: + return True + for v in obj.values(): + if contains_type(v, type_name): + return True + elif isinstance(obj, list): + for item in obj: + if contains_type(item, type_name): + return True + return False + + assert contains_type(result, "DeleteTriple") + assert contains_type(result, "AddTriple") + + def test_rdflist_swap_same_position(self): + """rdflist_swap with same positions should be a no-op (just type check).""" + query = WOQLQuery().lib().rdflist_swap("v:list_head", 1, 1) + result = query.to_dict() + + # Should contain a Triple check for rdf:type + def contains_triple(obj): + if isinstance(obj, dict): + if obj.get("@type") == "Triple": + return True + for v in obj.values(): + if contains_triple(v): + return True + elif isinstance(obj, list): + for item in obj: + if contains_triple(item): + return True + return False + + assert contains_triple(result) + + def test_rdflist_swap_negative_position_raises(self): + """rdflist_swap with negative position should raise ValueError.""" + with pytest.raises(ValueError, match="positions >= 0"): + WOQLQuery().lib().rdflist_swap("v:list", -1, 2) + + +class TestRDFListVariableScoping: + """Test that RDFList operations properly scope variables.""" + + def setup_method(self): + """Reset unique var counter before each test.""" + _reset_unique_var_counter(0) + + def test_rdflist_operations_use_unique_variables(self): + """Multiple rdflist calls should use different internal variables.""" + _reset_unique_var_counter(0) + + query1 = WOQLQuery().lib().rdflist_peek("v:list1", "v:val1") + + _reset_unique_var_counter(100) # Different starting point + query2 = WOQLQuery().lib().rdflist_peek("v:list2", "v:val2") + + result1 = query1.to_dict() + result2 = query2.to_dict() + + # Both should have valid structure + assert result1["@type"] == "And" + assert result2["@type"] == "And" + + # Internal variable names should be different (due to counter difference) + result1_str = str(result1) + result2_str = str(result2) + + # The unique counter values should differ + assert "_1" in result1_str or "_2" in result1_str + assert "_101" in result2_str or "_102" in result2_str + + def test_combined_rdflist_operations(self): + """Multiple rdflist operations can be combined in a query.""" + _reset_unique_var_counter(0) + + query = WOQLQuery().woql_and( + WOQLQuery().lib().rdflist_peek("v:list", "v:first"), + WOQLQuery().lib().rdflist_length("v:list", "v:len"), + ) + + result = query.to_dict() + assert result["@type"] == "And" + + # Should have multiple sub-queries + assert len(result.get("and", [])) >= 2 diff --git a/terminusdb_client/tests/test_woql_utils.py b/terminusdb_client/tests/test_woql_utils.py index 81ebfdc6..dfbb6b0e 100644 --- a/terminusdb_client/tests/test_woql_utils.py +++ b/terminusdb_client/tests/test_woql_utils.py @@ -1,4 +1,5 @@ """Tests for woql_utils.py module.""" + from datetime import datetime from unittest.mock import Mock import pytest @@ -10,7 +11,7 @@ _clean_list, _clean_dict, _dt_list, - _dt_dict + _dt_dict, ) from terminusdb_client.errors import DatabaseError @@ -37,7 +38,7 @@ def test_result2stream_with_whitespace(): def test_result2stream_empty(): """Test _result2stream with empty string.""" - result = '' + result = "" stream = list(_result2stream(result)) assert len(stream) == 0 @@ -125,11 +126,7 @@ def test_clean_list_with_datetime(): def test_clean_list_nested(): """Test _clean_list with nested structures.""" dt = datetime(2025, 1, 1) - obj = [ - "string", - {"key": dt}, - [1, 2, dt] - ] + obj = ["string", {"key": dt}, [1, 2, dt]] result = _clean_list(obj) @@ -141,11 +138,7 @@ def test_clean_list_nested(): def test_clean_dict_with_datetime(): """Test _clean_dict converts datetime to isoformat.""" dt = datetime(2025, 1, 1, 12, 0, 0) - obj = { - "date": dt, - "name": "test", - "count": 42 - } + obj = {"date": dt, "name": "test", "count": 42} result = _clean_dict(obj) @@ -157,11 +150,7 @@ def test_clean_dict_with_datetime(): def test_clean_dict_nested(): """Test _clean_dict with nested structures.""" dt = datetime(2025, 1, 1) - obj = { - "name": "test", - "nested": {"date": dt}, - "list": [1, dt, "string"] - } + obj = {"name": "test", "nested": {"date": dt}, "list": [1, dt, "string"]} result = _clean_dict(obj) @@ -172,10 +161,7 @@ def test_clean_dict_nested(): def test_clean_dict_with_iterable(): """Test _clean_dict handles iterables correctly.""" - obj = { - "tuple": (1, 2, 3), - "list": [4, 5, 6] - } + obj = {"tuple": (1, 2, 3), "list": [4, 5, 6]} result = _clean_dict(obj) @@ -197,11 +183,7 @@ def test_dt_list_parses_isoformat(): def test_dt_list_nested(): """Test _dt_list with nested structures.""" - obj = [ - "2025-01-01", - {"date": "2025-01-01T10:00:00"}, - ["2025-01-01", "text"] - ] + obj = ["2025-01-01", {"date": "2025-01-01T10:00:00"}, ["2025-01-01", "text"]] result = _dt_list(obj) @@ -216,11 +198,7 @@ def test_dt_list_nested(): def test_dt_dict_parses_isoformat(): """Test _dt_dict converts ISO format strings to datetime.""" - obj = { - "created": "2025-01-01T12:00:00", - "name": "test", - "invalid": "not a date" - } + obj = {"created": "2025-01-01T12:00:00", "name": "test", "invalid": "not a date"} result = _dt_dict(obj) @@ -235,7 +213,7 @@ def test_dt_dict_nested(): obj = { "name": "test", "nested": {"date": "2025-01-01"}, - "list": ["2025-01-01T10:00:00", 123] + "list": ["2025-01-01T10:00:00", 123], } result = _dt_dict(obj) @@ -248,10 +226,7 @@ def test_dt_dict_nested(): def test_dt_dict_with_iterable(): """Test _dt_dict handles iterables with dates.""" - obj = { - "dates": ["2025-01-01", "2025-01-02"], - "mixed": ["2025-01-01", "text", 123] - } + obj = {"dates": ["2025-01-01", "2025-01-02"], "mixed": ["2025-01-01", "text", 123]} result = _dt_dict(obj) @@ -264,10 +239,7 @@ def test_dt_dict_with_iterable(): def test_clean_list_handles_dict_items(): """Test _clean_list correctly identifies objects with items() method.""" dt = datetime(2025, 1, 1) - obj = [ - {"key1": "value1"}, - {"key2": dt} - ] + obj = [{"key1": "value1"}, {"key2": dt}] result = _clean_list(obj) @@ -277,10 +249,7 @@ def test_clean_list_handles_dict_items(): def test_dt_list_handles_dict_items(): """Test _dt_list correctly processes nested dicts.""" - obj = [ - {"date": "2025-01-01"}, - {"name": "test"} - ] + obj = [{"date": "2025-01-01"}, {"name": "test"}] result = _dt_list(obj) diff --git a/terminusdb_client/tests/test_woqldataframe.py b/terminusdb_client/tests/test_woqldataframe.py index 2dd8ae10..b725dc71 100644 --- a/terminusdb_client/tests/test_woqldataframe.py +++ b/terminusdb_client/tests/test_woqldataframe.py @@ -1,4 +1,5 @@ """Tests for woqldataframe/woqlDataframe.py module.""" + import pytest from unittest.mock import MagicMock, patch from terminusdb_client.woqldataframe.woqlDataframe import result_to_df @@ -7,7 +8,9 @@ def test_result_to_df_requires_pandas(): """Test that result_to_df raises ImportError when pandas is not available.""" - with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module') as mock_import: + with patch( + "terminusdb_client.woqldataframe.woqlDataframe.import_module" + ) as mock_import: mock_import.side_effect = ImportError("No module named 'pandas'") with pytest.raises(ImportError) as exc_info: @@ -20,9 +23,14 @@ def test_result_to_df_requires_pandas(): def test_result_to_df_requires_client_with_max_embed(): """Test that result_to_df raises ValueError when max_embed_dep > 0 without client.""" mock_pd = MagicMock() - mock_pd.DataFrame.return_value.from_records.return_value = mock_pd.DataFrame.return_value - - with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + mock_pd.DataFrame.return_value.from_records.return_value = ( + mock_pd.DataFrame.return_value + ) + + with patch( + "terminusdb_client.woqldataframe.woqlDataframe.import_module", + return_value=mock_pd, + ): with pytest.raises(ValueError) as exc_info: result_to_df([{"@id": "test", "@type": "Test"}], max_embed_dep=1) @@ -38,12 +46,14 @@ def test_result_to_df_multiple_types_error(): mock_df.__getitem__.return_value.unique.return_value = ["Type1", "Type2"] mock_pd.DataFrame.return_value.from_records.return_value = mock_df - with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + with patch( + "terminusdb_client.woqldataframe.woqlDataframe.import_module", + return_value=mock_pd, + ): with pytest.raises(ValueError) as exc_info: - result_to_df([ - {"@id": "test1", "@type": "Type1"}, - {"@id": "test2", "@type": "Type2"} - ]) + result_to_df( + [{"@id": "test1", "@type": "Type1"}, {"@id": "test2", "@type": "Type2"}] + ) assert "multiple type" in str(exc_info.value).lower() @@ -64,12 +74,15 @@ def test_result_to_df_class_not_in_schema(): mock_client.get_existing_classes.return_value = {"KnownClass": {}} mock_client.db = "testdb" - with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + with patch( + "terminusdb_client.woqldataframe.woqlDataframe.import_module", + return_value=mock_pd, + ): with pytest.raises(InterfaceError) as exc_info: result_to_df( [{"@id": "test1", "@type": "UnknownClass"}], max_embed_dep=1, - client=mock_client + client=mock_client, ) assert "UnknownClass" in str(exc_info.value) @@ -89,10 +102,13 @@ def test_result_to_df_basic_conversion(): mock_pd.DataFrame.return_value.from_records.return_value = mock_df - with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): - result = result_to_df([ - {"@id": "Person/Jane", "@type": "Person", "name": "Jane"} - ]) + with patch( + "terminusdb_client.woqldataframe.woqlDataframe.import_module", + return_value=mock_pd, + ): + result = result_to_df( + [{"@id": "Person/Jane", "@type": "Person", "name": "Jane"}] + ) # Should return the DataFrame assert result is not None @@ -109,10 +125,13 @@ def test_result_to_df_with_keepid(): mock_pd.DataFrame.return_value.from_records.return_value = mock_df - with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): - result = result_to_df([ - {"@id": "Person/Jane", "@type": "Person", "name": "Jane"} - ], keepid=True) + with patch( + "terminusdb_client.woqldataframe.woqlDataframe.import_module", + return_value=mock_pd, + ): + result = result_to_df( + [{"@id": "Person/Jane", "@type": "Person", "name": "Jane"}], keepid=True + ) # Should return the DataFrame assert result is not None @@ -129,13 +148,16 @@ def test_result_to_df_requires_client_for_embedding(): mock_df.__getitem__.return_value.unique.return_value = ["Person"] mock_pd.DataFrame.return_value.from_records.return_value = mock_df - with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): + with patch( + "terminusdb_client.woqldataframe.woqlDataframe.import_module", + return_value=mock_pd, + ): # This tests the ValueError raised at line 18-21 with pytest.raises(ValueError) as exc_info: result_to_df( [{"@id": "Person/Jane", "@type": "Person"}], max_embed_dep=2, # Requires client - client=None # But no client provided + client=None, # But no client provided ) assert "client need to be provide" in str(exc_info.value) @@ -160,14 +182,19 @@ def test_result_to_df_expand_nested_json(): mock_pd.DataFrame.return_value.from_records.return_value = mock_df - with patch('terminusdb_client.woqldataframe.woqlDataframe.import_module', return_value=mock_pd): - result = result_to_df([ - { - "@id": "Person/Jane", - "@type": "Person", - "address": {"@id": "Address/1", "street": "Main St"} - } - ]) + with patch( + "terminusdb_client.woqldataframe.woqlDataframe.import_module", + return_value=mock_pd, + ): + result = result_to_df( + [ + { + "@id": "Person/Jane", + "@type": "Person", + "address": {"@id": "Address/1", "street": "Main St"}, + } + ] + ) # json_normalize should be called for expansion assert mock_pd.json_normalize.called diff --git a/terminusdb_client/tests/woql_test_helpers.py b/terminusdb_client/tests/woql_test_helpers.py index 5f996413..bbf65885 100644 --- a/terminusdb_client/tests/woql_test_helpers.py +++ b/terminusdb_client/tests/woql_test_helpers.py @@ -3,6 +3,7 @@ This module provides standardized mocks and assertion helpers to verify that WOQLQuery methods generate correct JSON-LD structures. """ + from unittest.mock import Mock @@ -19,42 +20,53 @@ def create_mock_client(): @staticmethod def assert_query_type(query, expected_type): """Assert that query has the expected @type.""" - assert query._query.get("@type") == expected_type, \ - f"Expected @type={expected_type}, got {query._query.get('@type')}" + assert ( + query._query.get("@type") == expected_type + ), f"Expected @type={expected_type}, got {query._query.get('@type')}" @staticmethod def assert_has_key(query, key): """Assert that query has a specific key.""" - assert key in query._query, \ - f"Expected key '{key}' in query, got keys: {list(query._query.keys())}" + assert ( + key in query._query + ), f"Expected key '{key}' in query, got keys: {list(query._query.keys())}" @staticmethod def assert_key_value(query, key, expected_value): """Assert that query has a key with expected value.""" actual = query._query.get(key) - assert actual == expected_value, \ - f"Expected {key}={expected_value}, got {actual}" + assert ( + actual == expected_value + ), f"Expected {key}={expected_value}, got {actual}" @staticmethod def assert_is_variable(obj, expected_name=None): """Assert that an object is a variable structure.""" assert isinstance(obj, dict), f"Expected dict, got {type(obj)}" - assert obj.get("@type") in ["Value", "NodeValue", "DataValue", "ArithmeticValue"], \ - f"Expected variable type, got {obj.get('@type')}" + assert obj.get("@type") in [ + "Value", + "NodeValue", + "DataValue", + "ArithmeticValue", + ], f"Expected variable type, got {obj.get('@type')}" assert "variable" in obj, f"Expected 'variable' key in {obj}" if expected_name: - assert obj["variable"] == expected_name, \ - f"Expected variable name '{expected_name}', got '{obj['variable']}'" + assert ( + obj["variable"] == expected_name + ), f"Expected variable name '{expected_name}', got '{obj['variable']}'" @staticmethod def assert_is_node(obj, expected_node=None): """Assert that an object is a node structure.""" assert isinstance(obj, dict), f"Expected dict, got {type(obj)}" - assert "node" in obj or obj.get("@type") in ["Value", "NodeValue"], \ - f"Expected node structure, got {obj}" + assert "node" in obj or obj.get("@type") in [ + "Value", + "NodeValue", + ], f"Expected node structure, got {obj}" if expected_node and "node" in obj: - assert obj["node"] == expected_node, \ - f"Expected node '{expected_node}', got '{obj['node']}'" + assert ( + obj["node"] == expected_node + ), f"Expected node '{expected_node}', got '{obj['node']}'" @staticmethod def assert_is_data_value(obj, expected_type=None, expected_value=None): @@ -65,11 +77,13 @@ def assert_is_data_value(obj, expected_type=None, expected_value=None): assert "@type" in data, f"Expected '@type' in data: {data}" assert "@value" in data, f"Expected '@value' in data: {data}" if expected_type: - assert data["@type"] == expected_type, \ - f"Expected type '{expected_type}', got '{data['@type']}'" + assert ( + data["@type"] == expected_type + ), f"Expected type '{expected_type}', got '{data['@type']}'" if expected_value is not None: - assert data["@value"] == expected_value, \ - f"Expected value '{expected_value}', got '{data['@value']}'" + assert ( + data["@value"] == expected_value + ), f"Expected value '{expected_value}', got '{data['@value']}'" @staticmethod def get_query_dict(query): @@ -77,7 +91,9 @@ def get_query_dict(query): return query._query @staticmethod - def assert_triple_structure(query, check_subject=True, check_predicate=True, check_object=True): + def assert_triple_structure( + query, check_subject=True, check_predicate=True, check_object=True + ): """Assert that query has proper triple structure.""" WOQLTestHelpers.assert_query_type(query, "Triple") if check_subject: @@ -102,10 +118,13 @@ def assert_and_structure(query, expected_count=None): WOQLTestHelpers.assert_query_type(query, "And") WOQLTestHelpers.assert_has_key(query, "and") and_list = query._query["and"] - assert isinstance(and_list, list), f"Expected 'and' to be list, got {type(and_list)}" + assert isinstance( + and_list, list + ), f"Expected 'and' to be list, got {type(and_list)}" if expected_count is not None: - assert len(and_list) == expected_count, \ - f"Expected {expected_count} items in 'and', got {len(and_list)}" + assert ( + len(and_list) == expected_count + ), f"Expected {expected_count} items in 'and', got {len(and_list)}" @staticmethod def assert_or_structure(query, expected_count=None): @@ -113,10 +132,13 @@ def assert_or_structure(query, expected_count=None): WOQLTestHelpers.assert_query_type(query, "Or") WOQLTestHelpers.assert_has_key(query, "or") or_list = query._query["or"] - assert isinstance(or_list, list), f"Expected 'or' to be list, got {type(or_list)}" + assert isinstance( + or_list, list + ), f"Expected 'or' to be list, got {type(or_list)}" if expected_count is not None: - assert len(or_list) == expected_count, \ - f"Expected {expected_count} items in 'or', got {len(or_list)}" + assert ( + len(or_list) == expected_count + ), f"Expected {expected_count} items in 'or', got {len(or_list)}" @staticmethod def assert_select_structure(query): diff --git a/terminusdb_client/woql_type.py b/terminusdb_client/woql_type.py index a67ed977..b198a7aa 100644 --- a/terminusdb_client/woql_type.py +++ b/terminusdb_client/woql_type.py @@ -2,35 +2,35 @@ from enum import Enum from typing import ForwardRef, List, Optional, Set, Union, NewType -anyURI = NewType('anyURI', str) # noqa: N816 -anySimpleType = NewType('anySimpleType', str) # noqa: N816 -decimal = NewType('decimal', str) -dateTimeStamp = NewType('dateTimeStamp', dt.datetime) # noqa: N816 -gYear = NewType('gYear', str) # noqa: N816 -gMonth = NewType('gMonth', str) # noqa: N816 -gDay = NewType('gDay', str) # noqa: N816 -gYearMonth = NewType('gYearMonth', str) # noqa: N816 -yearMonthDuration = NewType('yearMonthDuration', str) # noqa: N816 -dayTimeDuration = NewType('dayTimeDuration', str) # noqa: N816 -byte = NewType('byte', int) -short = NewType('short', int) -long = NewType('long', int) -unsignedByte = NewType('unsignedByte', int) # noqa: N816 -unsignedShort = NewType('unsignedShort', int) # noqa: N816 -unsignedInt = NewType('unsignedInt', int) # noqa: N816 -unsignedLong = NewType('unsignedLong', int) # noqa: N816 -positiveInteger = NewType('positiveInteger', int) # noqa: N816 -negativeInteger = NewType('negativeInteger', int) # noqa: N816 -nonPositiveInteger = NewType('nonPositiveInteger', int) # noqa: N816 -nonNegativeInteger = NewType('nonNegativeInteger', int) # noqa: N816 -base64Binary = NewType('base64Binary', str) # noqa: N816 -hexBinary = NewType('hexBinary', str) # noqa: N816 -language = NewType('language', str) -normalizedString = NewType('normalizedString', str) # noqa: N816 -token = NewType('token', str) -NMTOKEN = NewType('NMTOKEN', str) -Name = NewType('Name', str) -NCName = NewType('NCName', str) +anyURI = NewType("anyURI", str) # noqa: N816 +anySimpleType = NewType("anySimpleType", str) # noqa: N816 +decimal = NewType("decimal", str) +dateTimeStamp = NewType("dateTimeStamp", dt.datetime) # noqa: N816 +gYear = NewType("gYear", str) # noqa: N816 +gMonth = NewType("gMonth", str) # noqa: N816 +gDay = NewType("gDay", str) # noqa: N816 +gYearMonth = NewType("gYearMonth", str) # noqa: N816 +yearMonthDuration = NewType("yearMonthDuration", str) # noqa: N816 +dayTimeDuration = NewType("dayTimeDuration", str) # noqa: N816 +byte = NewType("byte", int) +short = NewType("short", int) +long = NewType("long", int) +unsignedByte = NewType("unsignedByte", int) # noqa: N816 +unsignedShort = NewType("unsignedShort", int) # noqa: N816 +unsignedInt = NewType("unsignedInt", int) # noqa: N816 +unsignedLong = NewType("unsignedLong", int) # noqa: N816 +positiveInteger = NewType("positiveInteger", int) # noqa: N816 +negativeInteger = NewType("negativeInteger", int) # noqa: N816 +nonPositiveInteger = NewType("nonPositiveInteger", int) # noqa: N816 +nonNegativeInteger = NewType("nonNegativeInteger", int) # noqa: N816 +base64Binary = NewType("base64Binary", str) # noqa: N816 +hexBinary = NewType("hexBinary", str) # noqa: N816 +language = NewType("language", str) +normalizedString = NewType("normalizedString", str) # noqa: N816 +token = NewType("token", str) +NMTOKEN = NewType("NMTOKEN", str) +Name = NewType("Name", str) +NCName = NewType("NCName", str) CONVERT_TYPE = { str: "xsd:string", @@ -42,35 +42,35 @@ dt.date: "xsd:date", dt.time: "xsd:time", dt.timedelta: "xsd:duration", - anyURI : "xsd:anyURI", - anySimpleType : "xsd:anySimpleType", - decimal : "xsd:decimal", - dateTimeStamp : "xsd:dateTimeStamp", - gYear : "xsd:gYear", - gMonth : "xsd:gMonth", - gDay : "xsd:gDay", - gYearMonth : "xsd:gYearMonth", - yearMonthDuration : "xsd:yearMonthDuration", - dayTimeDuration : "xsd:dayTimeDuration", - byte : "xsd:byte", - short : "xsd:short", - long : "xsd:long", - unsignedByte : "xsd:unsignedByte", - unsignedShort : "xsd:unsignedShort", - unsignedInt : "xsd:unsignedInt", - unsignedLong : "xsd:unsignedLong", - positiveInteger : "xsd:positiveInteger", - negativeInteger : "xsd:negativeInteger", - nonPositiveInteger : "xsd:nonPositiveInteger", - nonNegativeInteger : "xsd:nonNegativeInteger", - base64Binary : "xsd:base64Binary", - hexBinary : "xsd:hexBinary", - language : "xsd:language", - normalizedString : "xsd:normalizedString", - token : "xsd:token", - NMTOKEN : "xsd:NMTOKEN", - Name : "xsd:Name", - NCName : "xsd:NCName", + anyURI: "xsd:anyURI", + anySimpleType: "xsd:anySimpleType", + decimal: "xsd:decimal", + dateTimeStamp: "xsd:dateTimeStamp", + gYear: "xsd:gYear", + gMonth: "xsd:gMonth", + gDay: "xsd:gDay", + gYearMonth: "xsd:gYearMonth", + yearMonthDuration: "xsd:yearMonthDuration", + dayTimeDuration: "xsd:dayTimeDuration", + byte: "xsd:byte", + short: "xsd:short", + long: "xsd:long", + unsignedByte: "xsd:unsignedByte", + unsignedShort: "xsd:unsignedShort", + unsignedInt: "xsd:unsignedInt", + unsignedLong: "xsd:unsignedLong", + positiveInteger: "xsd:positiveInteger", + negativeInteger: "xsd:negativeInteger", + nonPositiveInteger: "xsd:nonPositiveInteger", + nonNegativeInteger: "xsd:nonNegativeInteger", + base64Binary: "xsd:base64Binary", + hexBinary: "xsd:hexBinary", + language: "xsd:language", + normalizedString: "xsd:normalizedString", + token: "xsd:token", + NMTOKEN: "xsd:NMTOKEN", + Name: "xsd:Name", + NCName: "xsd:NCName", } @@ -144,7 +144,8 @@ def from_woql_type( def datetime_to_woql(dt_obj): """Convert datetime objects into strings that is recognize by woql. - Do nothing and return the object as it if it is not one of the supported datetime object.""" + Do nothing and return the object as it if it is not one of the supported datetime object. + """ if ( isinstance(dt_obj, dt.datetime) or isinstance(dt_obj, dt.date) diff --git a/terminusdb_client/woqlclient/__init__.py b/terminusdb_client/woqlclient/__init__.py index fa2b79e4..6d7a7bb2 100644 --- a/terminusdb_client/woqlclient/__init__.py +++ b/terminusdb_client/woqlclient/__init__.py @@ -1,4 +1,5 @@ -import sys # noqa +import sys # noqa from ..client import GraphType, Patch, Client # noqa + WOQLClient = Client -sys.modules["terminusdb_client.woqlclient.woqlClient"] = Client # noqa +sys.modules["terminusdb_client.woqlclient.woqlClient"] = Client # noqa diff --git a/terminusdb_client/woqldataframe/woqlDataframe.py b/terminusdb_client/woqldataframe/woqlDataframe.py index 7edaf4db..86b33251 100644 --- a/terminusdb_client/woqldataframe/woqlDataframe.py +++ b/terminusdb_client/woqldataframe/woqlDataframe.py @@ -7,7 +7,8 @@ def result_to_df(all_records, keepid=False, max_embed_dep=0, client=None): """Turn result documents into pandas DataFrame, all documents should be the same type. - If max_embed_dep > 0, a client needs to be provided to get objects to embed in DataFrame.""" + If max_embed_dep > 0, a client needs to be provided to get objects to embed in DataFrame. + """ try: pd = import_module("pandas") except ImportError: diff --git a/terminusdb_client/woqlquery/__init__.py b/terminusdb_client/woqlquery/__init__.py index b9df830f..44149ffe 100644 --- a/terminusdb_client/woqlquery/__init__.py +++ b/terminusdb_client/woqlquery/__init__.py @@ -1 +1,19 @@ -from .woql_query import WOQLQuery, Var, Vars, Doc # noqa +from .woql_query import ( # noqa: F401 + WOQLQuery, + Var, + Vars, + VarsUnique, + Doc, + WOQLLibrary, + _reset_unique_var_counter, +) + +__all__ = [ + "WOQLQuery", + "Var", + "Vars", + "VarsUnique", + "Doc", + "WOQLLibrary", + "_reset_unique_var_counter", +] diff --git a/terminusdb_client/woqlquery/woql_core.py b/terminusdb_client/woqlquery/woql_core.py index f660617a..2840ed8f 100644 --- a/terminusdb_client/woqlquery/woql_core.py +++ b/terminusdb_client/woqlquery/woql_core.py @@ -156,7 +156,7 @@ def _copy_dict(orig, rollup=None): query = _copy_dict(part, rollup) if query: nuj[key] = query - elif hasattr(part, 'to_dict'): + elif hasattr(part, "to_dict"): nuj[key] = part.to_dict() else: nuj[key] = part diff --git a/terminusdb_client/woqlquery/woql_query.py b/terminusdb_client/woqlquery/woql_query.py index 60578772..79463df4 100644 --- a/terminusdb_client/woqlquery/woql_query.py +++ b/terminusdb_client/woqlquery/woql_query.py @@ -50,8 +50,7 @@ def __str__(self): return self.name def to_dict(self): - return {"@type": "Value", - "variable": self.name} + return {"@type": "Value", "variable": self.name} class Vars: @@ -60,6 +59,47 @@ def __init__(self, *args): setattr(self, arg, Var(arg)) +# Global counter for unique variable names +_unique_var_counter = 0 + + +def _reset_unique_var_counter(value=0): + """Reset the unique variable counter to a specific value. + + Parameters + ---------- + value : int + The value to reset the counter to (default 0) + """ + global _unique_var_counter + _unique_var_counter = value + + +class VarsUnique: + """Generate unique variable names to prevent variable name collisions. + + Unlike Vars which uses the provided names directly, VarsUnique appends + a unique counter suffix to each variable name. This is useful for + library functions that need to avoid variable name collisions with + the calling context. + + Example + ------- + >>> v = VarsUnique('x', 'y', 'z') + >>> print(v.x.name) # 'x_1' + >>> print(v.y.name) # 'y_2' + >>> v2 = VarsUnique('x') # Different call + >>> print(v2.x.name) # 'x_3' (different from v.x) + """ + + def __init__(self, *args): + global _unique_var_counter + for arg in args: + _unique_var_counter += 1 + unique_name = f"{arg}_{_unique_var_counter}" + setattr(self, arg, Var(unique_name)) + + class Doc: def __init__(self, dictionary): self.dictionary = dictionary @@ -593,7 +633,8 @@ def from_json(self, input_json): def _json(self, input_json=None): """converts back and forward from json if the argument is present, the current query is set to it, - if the argument is not present, the current json version of this query is returned""" + if the argument is not present, the current json version of this query is returned + """ if input_json: self.from_dict(json.loads(input_json)) return self @@ -3384,3 +3425,930 @@ def variables(self, *args): if len(vars_tuple) == 1: vars_tuple = vars_tuple[0] return vars_tuple + + def localize(self, param_spec): + """Build a localized scope for variables to prevent leaking local variables to outer scope. + + Returns a tuple (localized_fn, v) where: + - localized_fn: function that wraps queries with select("") and eq() bindings + - v: VarsUnique object with unique variable names for use in the inner query + + Parameters with non-None values are bound from outer scope via eq(). + Parameters with None values are local-only variables. + + Parameters + ---------- + param_spec : dict + Object mapping parameter names to values (or None for local vars) + + Returns + ------- + tuple + (localized_fn, v) where localized_fn wraps queries and v contains unique vars + + Example + ------- + >>> [localized, v] = WOQLQuery().localize({'consSubject': 'v:list', 'valueVar': 'v:val', 'last_cell': None}) + >>> query = localized( + ... WOQLQuery().woql_and( + ... WOQLQuery().triple(v.consSubject, 'rdf:type', 'rdf:List'), + ... WOQLQuery().triple(v.last_cell, 'rdf:rest', 'rdf:nil') + ... ) + ... ) + """ + param_names = list(param_spec.keys()) + v = VarsUnique(*param_names) + + def localized_fn(query=None): + # Create eq bindings for outer parameters OUTSIDE select("") + # This ensures outer parameters are visible in query results + outer_eq_bindings = [] + for param_name in param_names: + outer_value = param_spec[param_name] + if outer_value is not None: + # If the outer value is a variable, add eq(var, var) to register it in outer scope + is_var = isinstance(outer_value, Var) or ( + isinstance(outer_value, str) and outer_value.startswith("v:") + ) + if is_var: + outer_eq_bindings.append( + WOQLQuery().eq(outer_value, outer_value) + ) + # Bind the unique variable to the outer parameter OUTSIDE the select("") + outer_eq_bindings.append( + WOQLQuery().eq(getattr(v, param_name), outer_value) + ) + + if query is not None: + # Functional mode: wrap query in select(""), then add outer eq bindings + localized_query = WOQLQuery().select(query) + + if outer_eq_bindings: + # Wrap: eq(outer) AND select("") { query } + return WOQLQuery().woql_and(*outer_eq_bindings, localized_query) + return localized_query + + # Fluent mode: return wrapper that applies pattern on woql_and() + class FluentWrapper(WOQLQuery): + def __init__(wrapper_self, outer_bindings, parent_localized): + super().__init__() + wrapper_self._outer_eq = outer_bindings + wrapper_self._parent_localized = parent_localized + + def woql_and(wrapper_self, *args): + inner_query = WOQLQuery().woql_and(*args) + localized_query = WOQLQuery().select(inner_query) + + if wrapper_self._outer_eq: + return WOQLQuery().woql_and( + *wrapper_self._outer_eq, localized_query + ) + return localized_query + + return FluentWrapper(outer_eq_bindings, self) + + return (localized_fn, v) + + def lib(self): + """Returns the WOQL library instance for RDFList operations and other library functions. + + Returns + ------- + WOQLLibrary + Library object with rdflist_* methods + + Example + ------- + >>> # Get the first element of an rdf:List + >>> query = WOQLQuery().lib().rdflist_peek('v:list_head', 'v:first_value') + """ + return WOQLLibrary() + + +class WOQLLibrary: + """Library functions for WOQL including RDF List operations. + + This class provides higher-level WOQL operations built on top of the + core WOQL primitives. RDFList operations work with rdf:List structures + using rdf:first, rdf:rest, and rdf:nil predicates. + """ + + def _rdflist_peek_raw(self, cons_subject, value_var): + """Internal: Get first element without localization.""" + return WOQLQuery().woql_and( + WOQLQuery().triple(cons_subject, "rdf:type", "rdf:List"), + WOQLQuery().triple(cons_subject, "rdf:first", value_var), + ) + + def _rdflist_member_raw(self, value, cons_subject, cell_var): + """Internal: Check if value is member of list without localization.""" + return WOQLQuery().woql_and( + WOQLQuery().path(cons_subject, "rdf:rest*", cell_var), + WOQLQuery().triple(cell_var, "rdf:first", value), + ) + + def _rdflist_length_raw(self, cons_subject, length_var, path_var): + """Internal: Get list length without localization.""" + return WOQLQuery().woql_and( + WOQLQuery().triple(cons_subject, "rdf:type", "rdf:List"), + WOQLQuery().path(cons_subject, "rdf:rest*", "rdf:nil", path_var), + WOQLQuery().length(path_var, length_var), + ) + + def _rdflist_nth0_raw(self, cons_subject, index, value_var): + """Internal: Get element at 0-indexed position without localization.""" + if index == 0: + return self._rdflist_peek_raw(cons_subject, value_var) + path_pattern = f"rdf:rest{{{index},{index}}}" + return WOQLQuery().woql_and( + WOQLQuery().triple(cons_subject, "rdf:type", "rdf:List"), + WOQLQuery().path(cons_subject, path_pattern, "v:_nth_cell"), + WOQLQuery().triple("v:_nth_cell", "rdf:first", value_var), + ) + + def rdflist_list(self, cons_subject, list_var): + """Collect all rdf:List elements into a single array. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list cons cell (rdf:List head) + list_var : str or Var + Variable to bind the resulting list values array + + Returns + ------- + WOQLQuery + Query that binds list as array to list_var + + Example + ------- + >>> query = WOQLQuery().woql_and( + ... WOQLQuery().triple('doc:mylist', 'tasks', 'v:list_head'), + ... WOQLQuery().lib().rdflist_list('v:list_head', 'v:all_values') + ... ) + >>> result = client.query(query) + >>> print(result['bindings'][0]['all_values']) # ['Task A', 'Task B', 'Task C'] + """ + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "listVar": list_var, + "cell": None, + "value": None, + } + ) + return localized( + WOQLQuery().group_by( + [], + [v.value], + v.listVar, + WOQLQuery().woql_and( + WOQLQuery().path(v.consSubject, "rdf:rest*", v.cell), + WOQLQuery().triple(v.cell, "rdf:first", v.value), + ), + ) + ) + + def rdflist_peek(self, cons_subject, value_var): + """Get the first element of an rdf:List (peek operation). + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list cons cell + value_var : str or Var + Variable to bind the first value + + Returns + ------- + WOQLQuery + Query that binds first value to value_var + + Example + ------- + >>> query = WOQLQuery().lib().rdflist_peek('v:list_head', 'v:first_value') + >>> result = client.query(query) + >>> print(result['bindings'][0]['first_value']) # First element + """ + (localized, v) = WOQLQuery().localize( + {"consSubject": cons_subject, "valueVar": value_var} + ) + return localized(self._rdflist_peek_raw(v.consSubject, v.valueVar)) + + def rdflist_last(self, cons_subject, value_var): + """Get the last element of an rdf:List. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list cons cell + value_var : str or Var + Variable to bind the last value + + Returns + ------- + WOQLQuery + Query that binds last value to value_var + + Example + ------- + >>> query = WOQLQuery().lib().rdflist_last('v:list_head', 'v:last_value') + >>> result = client.query(query) + >>> print(result['bindings'][0]['last_value']) # Last element + """ + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "valueVar": value_var, + "last_cell": None, + } + ) + return localized( + WOQLQuery().woql_and( + WOQLQuery().triple(v.consSubject, "rdf:type", "rdf:List"), + WOQLQuery().path(v.consSubject, "rdf:rest*", v.last_cell), + WOQLQuery().triple(v.last_cell, "rdf:rest", "rdf:nil"), + WOQLQuery().triple(v.last_cell, "rdf:first", v.valueVar), + ) + ) + + def rdflist_nth0(self, cons_subject, index, value_var): + """Get element at 0-indexed position in an rdf:List. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list cons cell + index : int or str or Var + 0-based index (number or variable) + value_var : str or Var + Variable to bind the value at that index + + Returns + ------- + WOQLQuery + Query that binds value at index to value_var + """ + if isinstance(index, int): + if index < 0: + raise ValueError("rdflist_nth0 requires index >= 0.") + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "valueVar": value_var, + } + ) + return localized(self._rdflist_nth0_raw(v.consSubject, index, v.valueVar)) + + # Dynamic index via variable + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "index": index, + "valueVar": value_var, + "cell": None, + "path": None, + } + ) + return localized( + WOQLQuery().woql_and( + WOQLQuery().triple(v.consSubject, "rdf:type", "rdf:List"), + WOQLQuery().path(v.consSubject, "rdf:rest*", v.cell, v.path), + WOQLQuery().length(v.path, v.index), + WOQLQuery().triple(v.cell, "rdf:first", v.valueVar), + ) + ) + + def rdflist_nth1(self, cons_subject, index, value_var): + """Get element at 1-indexed position in an rdf:List. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list cons cell + index : int or str or Var + 1-based index (number or variable) + value_var : str or Var + Variable to bind the value at that index + + Returns + ------- + WOQLQuery + Query that binds value at index to value_var + """ + if isinstance(index, int): + if index < 1: + raise ValueError("rdflist_nth1 requires index >= 1.") + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "valueVar": value_var, + } + ) + return localized( + self._rdflist_nth0_raw(v.consSubject, index - 1, v.valueVar) + ) + + raise ValueError( + "rdflist_nth1 with variable index not yet supported with localize pattern" + ) + + def rdflist_member(self, cons_subject, value): + """Traverse an rdf:List and yield each element as a separate binding. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list cons cell (rdf:List head) + value : str or Var + Variable to bind each value, or value to check membership + + Returns + ------- + WOQLQuery + Query that yields/matches list elements + + Example + ------- + >>> # Get all elements as separate bindings + >>> query = WOQLQuery().lib().rdflist_member('v:list_head', 'v:item') + >>> result = client.query(query) + >>> for binding in result['bindings']: + ... print(binding['item']) # Each element in turn + """ + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "value": value, + "member_cell": None, + } + ) + return localized( + WOQLQuery().woql_and( + WOQLQuery().triple(v.consSubject, "rdf:type", "rdf:List"), + self._rdflist_member_raw(v.value, v.consSubject, v.member_cell), + ) + ) + + def rdflist_length(self, cons_subject, length_var): + """Get the length of an rdf:List. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list cons cell + length_var : str or Var + Variable to bind the length + + Returns + ------- + WOQLQuery + Query that binds list length to length_var + + Example + ------- + >>> query = WOQLQuery().lib().rdflist_length('v:list_head', 'v:count') + >>> result = client.query(query) + >>> print(result['bindings'][0]['count']) # e.g., 3 + """ + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "lengthVar": length_var, + "length_path": None, + } + ) + return localized( + self._rdflist_length_raw(v.consSubject, v.lengthVar, v.length_path) + ) + + def rdflist_pop(self, cons_subject, value_var): + """Pop the first element from an rdf:List in-place. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list cons cell + value_var : str or Var + Variable to bind the popped value + + Returns + ------- + WOQLQuery + Query that pops first element and binds it to value_var + + Example + ------- + >>> # Pop first element: [A, B, C] -> [B, C], returns A + >>> query = WOQLQuery().lib().rdflist_pop('v:list_head', 'v:popped') + >>> result = client.query(query) + >>> print(result['bindings'][0]['popped']) # 'A' + """ + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "valueVar": value_var, + "second_cons": None, + "new_first": None, + "new_rest": None, + } + ) + return localized( + WOQLQuery().woql_and( + WOQLQuery().triple(v.consSubject, "rdf:first", v.valueVar), + WOQLQuery().triple(v.consSubject, "rdf:rest", v.second_cons), + WOQLQuery().triple(v.second_cons, "rdf:first", v.new_first), + WOQLQuery().triple(v.second_cons, "rdf:rest", v.new_rest), + WOQLQuery().delete_triple(v.second_cons, "rdf:type", "rdf:List"), + WOQLQuery().delete_triple(v.second_cons, "rdf:first", v.new_first), + WOQLQuery().delete_triple(v.second_cons, "rdf:rest", v.new_rest), + WOQLQuery().delete_triple(v.consSubject, "rdf:first", v.valueVar), + WOQLQuery().delete_triple(v.consSubject, "rdf:rest", v.second_cons), + WOQLQuery().add_triple(v.consSubject, "rdf:first", v.new_first), + WOQLQuery().add_triple(v.consSubject, "rdf:rest", v.new_rest), + ) + ) + + def rdflist_push(self, cons_subject, value): + """Push a new element to the front of an rdf:List in-place. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list cons cell + value : str or Var or dict + Value to push to the front + + Returns + ------- + WOQLQuery + Query that pushes value to front of list in-place + + Example + ------- + >>> # Push 'New Item' to front: [A, B] -> [New Item, A, B] + >>> query = WOQLQuery().lib().rdflist_push('v:list_head', + ... WOQLQuery().string('New Item')) + >>> client.query(query) + """ + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "value": value, + "old_first": None, + "old_rest": None, + "new_cons": None, + } + ) + return localized( + WOQLQuery().woql_and( + WOQLQuery().triple(v.consSubject, "rdf:first", v.old_first), + WOQLQuery().triple(v.consSubject, "rdf:rest", v.old_rest), + WOQLQuery().idgen_random("terminusdb://data/Cons/", v.new_cons), + WOQLQuery().add_triple(v.new_cons, "rdf:type", "rdf:List"), + WOQLQuery().add_triple(v.new_cons, "rdf:first", v.old_first), + WOQLQuery().add_triple(v.new_cons, "rdf:rest", v.old_rest), + WOQLQuery().delete_triple(v.consSubject, "rdf:first", v.old_first), + WOQLQuery().delete_triple(v.consSubject, "rdf:rest", v.old_rest), + WOQLQuery().add_triple(v.consSubject, "rdf:first", v.value), + WOQLQuery().add_triple(v.consSubject, "rdf:rest", v.new_cons), + ) + ) + + def rdflist_append(self, cons_subject, value, new_cell=None): + """Append an element to the end of an rdf:List. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list head + value : str or Var or dict + Value to append + new_cell : str or Var, optional + Variable to bind the new cell + + Returns + ------- + WOQLQuery + Query that appends value to end of list + + Example + ------- + >>> # Append 'Last Item' to end: [A, B] -> [A, B, Last Item] + >>> query = WOQLQuery().lib().rdflist_append('v:list_head', + ... WOQLQuery().string('Last Item'), 'v:new_cell') + >>> client.query(query) + """ + if new_cell is None: + new_cell = "v:_append_new_cell" + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "value": value, + "newCell": new_cell, + "last_cell": None, + } + ) + return localized( + WOQLQuery().woql_and( + WOQLQuery().triple(v.consSubject, "rdf:type", "rdf:List"), + WOQLQuery().path(v.consSubject, "rdf:rest*", v.last_cell), + WOQLQuery().triple(v.last_cell, "rdf:rest", "rdf:nil"), + WOQLQuery().idgen_random("terminusdb://data/Cons/", v.newCell), + WOQLQuery().delete_triple(v.last_cell, "rdf:rest", "rdf:nil"), + WOQLQuery().add_triple(v.last_cell, "rdf:rest", v.newCell), + WOQLQuery().add_triple(v.newCell, "rdf:type", "rdf:List"), + WOQLQuery().add_triple(v.newCell, "rdf:first", v.value), + WOQLQuery().add_triple(v.newCell, "rdf:rest", "rdf:nil"), + ) + ) + + def rdflist_clear(self, cons_subject, new_list_var): + """Delete all cons cells of an rdf:List and return rdf:nil as the new list value. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list head + new_list_var : str or Var + Variable to bind rdf:nil (the empty list) + + Returns + ------- + WOQLQuery + Query that deletes all cons cells and binds rdf:nil + + Example + ------- + >>> # Clear the list and update the reference + >>> query = WOQLQuery().woql_and( + ... WOQLQuery().lib().rdflist_clear('v:list_head', 'v:empty'), + ... WOQLQuery().delete_triple('doc:obj', 'tasks', 'v:list_head'), + ... WOQLQuery().add_triple('doc:obj', 'tasks', 'v:empty') + ... ) + >>> client.query(query) + """ + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "newListVar": new_list_var, + "cell": None, + "cell_first": None, + "cell_rest": None, + "head_first": None, + "head_rest": None, + } + ) + return localized( + WOQLQuery().woql_and( + # Bind the new list value (rdf:nil = empty list) + WOQLQuery().eq(v.newListVar, "rdf:nil"), + # Delete all triples from tail cons cells + WOQLQuery().opt( + WOQLQuery().woql_and( + WOQLQuery().path(v.consSubject, "rdf:rest+", v.cell), + WOQLQuery().triple(v.cell, "rdf:type", "rdf:List"), + WOQLQuery().triple(v.cell, "rdf:first", v.cell_first), + WOQLQuery().triple(v.cell, "rdf:rest", v.cell_rest), + WOQLQuery().delete_triple(v.cell, "rdf:type", "rdf:List"), + WOQLQuery().delete_triple(v.cell, "rdf:first", v.cell_first), + WOQLQuery().delete_triple(v.cell, "rdf:rest", v.cell_rest), + ) + ), + # Delete the head cons cell + WOQLQuery().triple(v.consSubject, "rdf:type", "rdf:List"), + WOQLQuery().triple(v.consSubject, "rdf:first", v.head_first), + WOQLQuery().triple(v.consSubject, "rdf:rest", v.head_rest), + WOQLQuery().delete_triple(v.consSubject, "rdf:type", "rdf:List"), + WOQLQuery().delete_triple(v.consSubject, "rdf:first", v.head_first), + WOQLQuery().delete_triple(v.consSubject, "rdf:rest", v.head_rest), + ) + ) + + def rdflist_empty(self, list_var): + """Create an empty rdf:List (just rdf:nil). + + Parameters + ---------- + list_var : str or Var + Variable to bind to rdf:nil + + Returns + ------- + WOQLQuery + Query that creates empty list + """ + return WOQLQuery().eq(list_var, "rdf:nil") + + def rdflist_is_empty(self, cons_subject): + """Check if an rdf:List is empty (equals rdf:nil). + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI to check + + Returns + ------- + WOQLQuery + Query that succeeds if list is empty + """ + return WOQLQuery().eq(cons_subject, "rdf:nil") + + def rdflist_slice(self, cons_subject, start, end, result_var): + """Extract a slice of an rdf:List (range of elements) as an array. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list head + start : int + Start index (inclusive) + end : int + End index (exclusive) + result_var : str or Var + Variable to bind the array of sliced values + + Returns + ------- + WOQLQuery + Query that extracts list slice values + + Example + ------- + >>> # Get elements 1-3 (0-indexed): [A, B, C, D] -> [B, C] + >>> query = WOQLQuery().lib().rdflist_slice('v:list_head', 1, 3, 'v:slice') + >>> result = client.query(query) + >>> print(result['bindings'][0]['slice']) # ['B', 'C'] + """ + if start < 0 or end < 0: + raise ValueError("rdflist_slice: negative indices not supported") + + if start >= end: + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "resultVar": result_var, + } + ) + return localized(WOQLQuery().eq(v.resultVar, [])) + + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "resultVar": result_var, + "node": None, + "value": None, + } + ) + + if start == 0 and end == 1: + find_nodes = WOQLQuery().eq(v.node, v.consSubject) + elif start == 0: + find_nodes = WOQLQuery().path( + v.consSubject, f"rdf:rest{{0,{end - 1}}}", v.node + ) + else: + find_nodes = WOQLQuery().path( + v.consSubject, f"rdf:rest{{{start},{end - 1}}}", v.node + ) + + return localized( + WOQLQuery().group_by( + [], + [v.value], + v.resultVar, + WOQLQuery().woql_and( + find_nodes, WOQLQuery().triple(v.node, "rdf:first", v.value) + ), + ) + ) + + def rdflist_insert(self, cons_subject, position, value, new_node_var=None): + """Insert a value at a specific position in an rdf:List. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list head + position : int + Position to insert at (0-indexed) + value : str or Var or dict + Value to insert + new_node_var : str or Var, optional + Variable to bind the new node + + Returns + ------- + WOQLQuery + Query that inserts value at position + """ + if position < 0: + raise ValueError("rdflist_insert requires position >= 0.") + + if new_node_var is None: + new_node_var = "v:_insert_new_node" + + if position == 0: + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "value": value, + "newNodeVar": new_node_var, + "old_first": None, + "old_rest": None, + } + ) + return localized( + WOQLQuery().woql_and( + WOQLQuery().triple(v.consSubject, "rdf:first", v.old_first), + WOQLQuery().triple(v.consSubject, "rdf:rest", v.old_rest), + WOQLQuery().idgen( + "list_node", [v.old_first, v.consSubject], v.newNodeVar + ), + WOQLQuery().add_triple(v.newNodeVar, "rdf:type", "rdf:List"), + WOQLQuery().add_triple(v.newNodeVar, "rdf:first", v.old_first), + WOQLQuery().add_triple(v.newNodeVar, "rdf:rest", v.old_rest), + WOQLQuery().delete_triple(v.consSubject, "rdf:first", v.old_first), + WOQLQuery().delete_triple(v.consSubject, "rdf:rest", v.old_rest), + WOQLQuery().add_triple(v.consSubject, "rdf:first", v.value), + WOQLQuery().add_triple(v.consSubject, "rdf:rest", v.newNodeVar), + ) + ) + + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "value": value, + "newNodeVar": new_node_var, + "pred_node": None, + "old_rest": None, + } + ) + rest_count = position - 1 + path_pattern = ( + "" if rest_count == 0 else f"rdf:rest{{{rest_count},{rest_count}}}" + ) + + if rest_count == 0: + find_predecessor = WOQLQuery().eq(v.pred_node, v.consSubject) + else: + find_predecessor = WOQLQuery().path( + v.consSubject, path_pattern, v.pred_node + ) + + return localized( + WOQLQuery().woql_and( + find_predecessor, + WOQLQuery().triple(v.pred_node, "rdf:rest", v.old_rest), + WOQLQuery().idgen("list_node", [v.value, v.pred_node], v.newNodeVar), + WOQLQuery().delete_triple(v.pred_node, "rdf:rest", v.old_rest), + WOQLQuery().add_triple(v.pred_node, "rdf:rest", v.newNodeVar), + WOQLQuery().add_triple(v.newNodeVar, "rdf:type", "rdf:List"), + WOQLQuery().add_triple(v.newNodeVar, "rdf:first", v.value), + WOQLQuery().add_triple(v.newNodeVar, "rdf:rest", v.old_rest), + ) + ) + + def rdflist_drop(self, cons_subject, position): + """Drop/remove a single element from an rdf:List at a specific position. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list head + position : int + Position of element to remove (0-indexed) + + Returns + ------- + WOQLQuery + Query that removes the element at position + """ + if position < 0: + raise ValueError("rdflist_drop requires position >= 0.") + + if position == 0: + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "old_first": None, + "rest_node": None, + "next_first": None, + "next_rest": None, + } + ) + return localized( + WOQLQuery().woql_and( + WOQLQuery().triple(v.consSubject, "rdf:first", v.old_first), + WOQLQuery().triple(v.consSubject, "rdf:rest", v.rest_node), + WOQLQuery().triple(v.rest_node, "rdf:first", v.next_first), + WOQLQuery().triple(v.rest_node, "rdf:rest", v.next_rest), + WOQLQuery().delete_triple(v.rest_node, "rdf:type", "rdf:List"), + WOQLQuery().delete_triple(v.rest_node, "rdf:first", v.next_first), + WOQLQuery().delete_triple(v.rest_node, "rdf:rest", v.next_rest), + WOQLQuery().delete_triple(v.consSubject, "rdf:first", v.old_first), + WOQLQuery().delete_triple(v.consSubject, "rdf:rest", v.rest_node), + WOQLQuery().add_triple(v.consSubject, "rdf:first", v.next_first), + WOQLQuery().add_triple(v.consSubject, "rdf:rest", v.next_rest), + ) + ) + + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "pred_node": None, + "drop_node": None, + "drop_first": None, + "drop_rest": None, + } + ) + rest_count = position - 1 + path_pattern = ( + "" if rest_count == 0 else f"rdf:rest{{{rest_count},{rest_count}}}" + ) + + if rest_count == 0: + find_predecessor = WOQLQuery().eq(v.pred_node, v.consSubject) + else: + find_predecessor = WOQLQuery().path( + v.consSubject, path_pattern, v.pred_node + ) + + return localized( + WOQLQuery().woql_and( + find_predecessor, + WOQLQuery().triple(v.pred_node, "rdf:rest", v.drop_node), + WOQLQuery().triple(v.drop_node, "rdf:first", v.drop_first), + WOQLQuery().triple(v.drop_node, "rdf:rest", v.drop_rest), + WOQLQuery().delete_triple(v.drop_node, "rdf:type", "rdf:List"), + WOQLQuery().delete_triple(v.drop_node, "rdf:first", v.drop_first), + WOQLQuery().delete_triple(v.drop_node, "rdf:rest", v.drop_rest), + WOQLQuery().delete_triple(v.pred_node, "rdf:rest", v.drop_node), + WOQLQuery().add_triple(v.pred_node, "rdf:rest", v.drop_rest), + ) + ) + + def rdflist_swap(self, cons_subject, pos_a, pos_b): + """Swap elements at two positions in an rdf:List. + + Parameters + ---------- + cons_subject : str or Var + Variable or IRI of the list head + pos_a : int + First position (0-indexed) + pos_b : int + Second position (0-indexed) + + Returns + ------- + WOQLQuery + Query that swaps elements at the two positions + """ + if pos_a < 0 or pos_b < 0: + raise ValueError("rdflist_swap requires positions >= 0.") + + if pos_a == pos_b: + (localized, v) = WOQLQuery().localize({"consSubject": cons_subject}) + return localized(WOQLQuery().triple(v.consSubject, "rdf:type", "rdf:List")) + + (localized, v) = WOQLQuery().localize( + { + "consSubject": cons_subject, + "node_a": None, + "node_b": None, + "value_a": None, + "value_b": None, + } + ) + + # Find node at pos_a + if pos_a == 0: + find_node_a = WOQLQuery().eq(v.node_a, v.consSubject) + else: + find_node_a = WOQLQuery().path( + v.consSubject, f"rdf:rest{{{pos_a},{pos_a}}}", v.node_a + ) + + # Find node at pos_b + if pos_b == 0: + find_node_b = WOQLQuery().eq(v.node_b, v.consSubject) + else: + find_node_b = WOQLQuery().path( + v.consSubject, f"rdf:rest{{{pos_b},{pos_b}}}", v.node_b + ) + + return localized( + WOQLQuery().woql_and( + find_node_a, + find_node_b, + WOQLQuery().triple(v.node_a, "rdf:first", v.value_a), + WOQLQuery().triple(v.node_b, "rdf:first", v.value_b), + WOQLQuery().delete_triple(v.node_a, "rdf:first", v.value_a), + WOQLQuery().delete_triple(v.node_b, "rdf:first", v.value_b), + WOQLQuery().add_triple(v.node_a, "rdf:first", v.value_b), + WOQLQuery().add_triple(v.node_b, "rdf:first", v.value_a), + ) + ) diff --git a/terminusdb_client/woqlschema/__init__.py b/terminusdb_client/woqlschema/__init__.py index a9f343b9..1410a51b 100644 --- a/terminusdb_client/woqlschema/__init__.py +++ b/terminusdb_client/woqlschema/__init__.py @@ -1,4 +1,5 @@ -import sys # noqa +import sys # noqa from ..schema import * # noqa -WOQLSchema = Schema # noqa -sys.modules["terminusdb_client.woqlschema.woql_schema"] = schema # noqa + +WOQLSchema = Schema # noqa +sys.modules["terminusdb_client.woqlschema.woql_schema"] = schema # noqa