diff --git a/terminusdb_client/__init__.py b/terminusdb_client/__init__.py index c4d79b5f..fe11f09d 100644 --- a/terminusdb_client/__init__.py +++ b/terminusdb_client/__init__.py @@ -1,4 +1,4 @@ -from .client import Patch, Client # noqa +from .client import GraphType, Patch, Client # noqa from .woqldataframe import woqlDataframe as WOQLDataFrame # noqa from .woqlquery import WOQLQuery # noqa from .woqlschema import * # noqa diff --git a/terminusdb_client/client/Client.py b/terminusdb_client/client/Client.py index b6d1eb3e..c50dd7cd 100644 --- a/terminusdb_client/client/Client.py +++ b/terminusdb_client/client/Client.py @@ -8,6 +8,7 @@ import warnings from collections.abc import Iterable from datetime import datetime +from enum import Enum from typing import Any, Dict, List, Optional, Union import requests @@ -20,6 +21,7 @@ _dt_list, _finish_response, _result2stream, + _args_as_payload, ) from ..woqlquery.woql_query import WOQLQuery @@ -119,6 +121,12 @@ def copy(self): return copy.deepcopy(self) +class GraphType(str, Enum): + """Type of graph""" + INSTANCE = 'instance' + SCHEMA = 'schema' + + class Client: """Client for TerminusDB server. @@ -336,15 +344,7 @@ def connect( self._connected = True try: - self._db_info = json.loads( - _finish_response( - requests.get( - self.api + "/info", - headers=self._default_headers, - auth=self._auth(), - ) - ) - ) + self._db_info = self.info() except Exception as error: raise InterfaceError( f"Cannot connect to server, please make sure TerminusDB is running at {self.server_url} and the authentication details are correct. Details: {str(error)}" @@ -379,6 +379,65 @@ def _check_connection(self, check_db=True) -> None: "No database is connected. Please either connect to a database or create a new database." ) + def info(self) -> dict: + """Get info of a TerminusDB database server + + Returns + ------- + dict + + Dict with version information: + ``` + { + "@type": "api:InfoResponse", + "api:info": { + "authority": "anonymous", + "storage": { + "version": "1" + }, + "terminusdb": { + "git_hash": "53acb38f9aedeec6c524f5679965488788e6ccf5", + "version": "10.1.5" + }, + "terminusdb_store": { + "version": "0.19.8" + } + }, + "api:status": "api:success" + } + ``` + """ + return json.loads( + _finish_response( + requests.get( + self.api + "/info", + headers=self._default_headers, + auth=self._auth(), + ) + ) + ) + + def ok(self) -> bool: + """Check whether the TerminusDB server is still OK. + Status is not OK when this function returns false + or throws an exception (mostly ConnectTimeout) + + Raises + ------ + Exception + When a connection can't be made by the requests library + + Returns + ------- + bool + """ + req = requests.get( + 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, @@ -713,17 +772,13 @@ def delete_database( ) self.db = None - def _validate_graph_type(self, graph_type): - if graph_type not in ["instance", "schema"]: - raise ValueError("graph_type can only be 'instance' or 'schema'") - - def get_triples(self, graph_type: str) -> str: + def get_triples(self, graph_type: GraphType) -> str: """Retrieves the contents of the specified graph as triples encoded in turtle format Parameters ---------- - graph_type : str - Graph type, either "instance" or "schema". + graph_type : GraphType + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. Raises ------ @@ -735,7 +790,6 @@ def get_triples(self, graph_type: str) -> str: str """ self._check_connection() - self._validate_graph_type(graph_type) result = requests.get( self._triples_url(graph_type), headers=self._default_headers, @@ -743,14 +797,14 @@ def get_triples(self, graph_type: str) -> str: ) return json.loads(_finish_response(result)) - def update_triples(self, graph_type: str, 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 Parameters ---------- - graph_type : str - Graph type, either "instance" or "schema". + graph_type : GraphType + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. content Valid set of triples in Turtle or Trig format. commit_msg : str @@ -762,7 +816,6 @@ def update_triples(self, graph_type: str, content: str, commit_msg: str) -> None if the client does not connect to a database """ self._check_connection() - self._validate_graph_type(graph_type) params = {"commit_info": self._generate_commit(commit_msg), "turtle": content, } @@ -775,14 +828,14 @@ def update_triples(self, graph_type: str, content: str, commit_msg: str) -> None return json.loads(_finish_response(result)) def insert_triples( - self, graph_type: str, content: str, commit_msg: Optional[str] = None + self, graph_type: GraphType, content: str, commit_msg: Optional[str] = None ) -> None: """Inserts into the specified graph with the triples encoded in turtle format. Parameters ---------- - graph_type : str - Graph type, either "instance" or "schema". + graph_type : GraphType + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. content Valid set of triples in Turtle or Trig format. commit_msg : str @@ -794,7 +847,6 @@ def insert_triples( if the client does not connect to a database """ self._check_connection() - self._validate_graph_type(graph_type) params = {"commit_info": self._generate_commit(commit_msg), "turtle": content } @@ -809,7 +861,7 @@ def insert_triples( def query_document( self, document_template: dict, - graph_type: str = "instance", + graph_type: GraphType = GraphType.INSTANCE, skip: int = 0, count: Optional[int] = None, as_list: bool = False, @@ -822,8 +874,8 @@ def query_document( ---------- document_template : dict Template for the document that is being retrived - graph_type : str, optional - Graph type, either "instance" or "schema". + graph_type : GraphType + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. as_list: bool If the result returned as list rather than an iterator. get_data_version: bool @@ -838,7 +890,6 @@ def query_document( ------- Iterable """ - self._validate_graph_type(graph_type) self._check_connection() payload = {"query": document_template, "graph_type": graph_type} @@ -874,7 +925,7 @@ def query_document( def get_document( self, iri_id: str, - graph_type: str = "instance", + graph_type: GraphType = GraphType.INSTANCE, get_data_version: bool = False, **kwargs, ) -> dict: @@ -884,8 +935,8 @@ def get_document( ---------- iri_id : str Iri id for the docuemnt that is retriving - graph_type : str, optional - Graph type, either "instance" or "schema". + graph_type : GraphType + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. get_data_version: bool If the data version of the document(s) should be obtained. If True, the method return the result and the version as a tuple. kwargs: @@ -900,8 +951,6 @@ def get_document( ------- dict """ - self._validate_graph_type(graph_type) - add_args = ["prefixed", "minimized", "unfold"] self._check_connection() payload = {"id": iri_id, "graph_type": graph_type} @@ -925,7 +974,7 @@ def get_document( def get_documents_by_type( self, doc_type: str, - graph_type: str = "instance", + graph_type: GraphType = GraphType.INSTANCE, skip: int = 0, count: Optional[int] = None, as_list: bool = False, @@ -938,8 +987,8 @@ def get_documents_by_type( ---------- doc_type : str Specific type for the docuemnts that is retriving - graph_type : str, optional - Graph type, either "instance" or "schema". + graph_type : GraphType, optional + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. skip: int The starting posiion of the returning results, default to be 0 count: int or None @@ -961,53 +1010,26 @@ def get_documents_by_type( iterable Stream of dictionaries """ - self._validate_graph_type(graph_type) - - add_args = ["prefixed", "unfold"] - self._check_connection() - payload = {"type": doc_type, "graph_type": graph_type} - payload["skip"] = skip - if count is not None: - payload["count"] = count - for the_arg in add_args: - if the_arg in kwargs: - payload[the_arg] = kwargs[the_arg] - result = requests.get( - self._documents_url(), - headers=self._default_headers, - params=payload, - auth=self._auth(), - ) - - if get_data_version: - result, version = _finish_response(result, get_data_version) - return_obj = _result2stream(result) - if as_list: - return list(return_obj), version - else: - return return_obj, version - - return_obj = _result2stream(_finish_response(result)) - if as_list: - return list(return_obj) - else: - return return_obj + return self.get_all_documents(graph_type, skip, count, + as_list, get_data_version, + doc_type=doc_type, **kwargs) def get_all_documents( self, - graph_type: str = "instance", + graph_type: GraphType = GraphType.INSTANCE, skip: int = 0, count: Optional[int] = None, as_list: bool = False, get_data_version: bool = False, + doc_type: Optional[str] = None, **kwargs, ) -> Union[Iterable, list, tuple]: """Retrieves all avalibale the documents Parameters ---------- - graph_type : str, optional - Graph type, either "instance" or "schema". + graph_type : GraphType, optional + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. skip: int The starting posiion of the returning results, default to be 0 count: int or None @@ -1029,14 +1051,13 @@ def get_all_documents( iterable Stream of dictionaries """ - self._validate_graph_type(graph_type) - add_args = ["prefixed", "unfold"] self._check_connection() - payload = {"graph_type": graph_type} - payload["skip"] = skip - if count is not None: - payload["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] @@ -1088,17 +1109,6 @@ def _conv_to_dict(self, obj): else: raise ValueError("Object cannot convert to dictionary") - def _ref_extract(self, target_key, search_item): - if hasattr(search_item, "items"): - for key, value in search_item.items(): - if key == target_key: - yield value - if isinstance(value, dict): - yield from self._ref_extract(target_key, value) - elif isinstance(value, list): - for item in value: - yield from self._ref_extract(target_key, item) - def _unseen(self, seen): unseen = [] for key in self._references: @@ -1140,7 +1150,7 @@ def insert_document( "DocumentTemplate", # noqa:F821 List["DocumentTemplate"], # noqa:F821 ], - graph_type: str = "instance", + graph_type: GraphType = GraphType.INSTANCE, full_replace: bool = False, commit_msg: Optional[str] = None, last_data_version: Optional[str] = None, @@ -1153,8 +1163,8 @@ def insert_document( ---------- document: dict or list of dict Document(s) to be inserted. - graph_type : str - Graph type, either "inference", "instance" or "schema". + graph_type : GraphType + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. full_replace:: bool If True then the whole graph will be replaced. WARNING: you should also supply the context object as the first element in the list of documents if using this option. commit_msg : str @@ -1176,7 +1186,6 @@ def insert_document( list list of ids of the inseted docuemnts """ - self._validate_graph_type(graph_type) self._check_connection() params = self._generate_commit(commit_msg) params["graph_type"] = graph_type @@ -1247,21 +1256,21 @@ def replace_document( "DocumentTemplate", # noqa:F821 List["DocumentTemplate"], # noqa:F821 ], - graph_type: str = "instance", + graph_type: GraphType = GraphType.INSTANCE, commit_msg: Optional[str] = None, last_data_version: Optional[str] = None, compress: Union[str, int] = 1024, create: bool = False, raw_json: bool = False, - ) -> None: + ) -> dict: """Updates the specified document(s) Parameters ---------- document: dict or list of dict Document(s) to be updated. - graph_type : str - Graph type, either "instance" or "schema". + graph_type : GraphType + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. commit_msg : str Commit message. last_data_version : str @@ -1278,7 +1287,6 @@ def replace_document( InterfaceError if the client does not connect to a database """ - self._validate_graph_type(graph_type) self._check_connection() params = self._generate_commit(commit_msg) params["graph_type"] = graph_type @@ -1330,7 +1338,7 @@ def update_document( "DocumentTemplate", # noqa:F821 List["DocumentTemplate"], # noqa:F821 ], - graph_type: str = "instance", + graph_type: GraphType = GraphType.INSTANCE, commit_msg: Optional[str] = None, last_data_version: Optional[str] = None, compress: Union[str, int] = 1024, @@ -1341,8 +1349,8 @@ def update_document( ---------- document: dict or list of dict Document(s) to be updated. - graph_type : str - Graph type, either "instance" or "schema". + graph_type : GraphType + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. commit_msg : str Commit message. last_data_version : str @@ -1362,7 +1370,7 @@ def update_document( def delete_document( self, document: Union[str, list, dict, Iterable], - graph_type: str = "instance", + graph_type: GraphType = GraphType.INSTANCE, commit_msg: Optional[str] = None, last_data_version: Optional[str] = None, ) -> None: @@ -1372,8 +1380,8 @@ def delete_document( ---------- document: str or list of str Document(s) (as dictionary or DocumentTemplate objects) or id(s) of document(s) to be updated. - graph_type : str - Graph type, either "instance" or "schema". + graph_type : GraphType + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. commit_msg : str Commit message. last_data_version : str @@ -1384,7 +1392,6 @@ def delete_document( InterfaceError if the client does not connect to a database """ - self._validate_graph_type(graph_type) self._check_connection() doc_id = [] if not isinstance(document, (str, list, dict)) and hasattr( @@ -1417,15 +1424,15 @@ def delete_document( ) ) - def has_doc(self, doc_id: str, graph_type: str = "instance") -> bool: + def has_doc(self, doc_id: str, graph_type: GraphType = GraphType.INSTANCE) -> bool: """Check if a certain document exist in a database Parameters ---------- doc_id: str Id of document to be checked. - graph_type : str - Graph type, either "instance" or "schema". + graph_type : GraphType + Graph type, either GraphType.INSTANCE or GraphType.SCHEMA. Raises ------ @@ -1437,7 +1444,6 @@ def has_doc(self, doc_id: str, graph_type: str = "instance") -> bool: Bool if the document exist """ - self._validate_graph_type(graph_type) self._check_connection() response = requests.get( @@ -2901,7 +2907,7 @@ def _documents_url(self): base_url = self._branch_base("document") return base_url - def _triples_url(self, graph_type="instance"): + def _triples_url(self, graph_type: GraphType = GraphType.INSTANCE): if self._db == "_system": base_url = self._db_base("triples") else: diff --git a/terminusdb_client/client/__init__.py b/terminusdb_client/client/__init__.py index 34865fcb..b04773b7 100644 --- a/terminusdb_client/client/__init__.py +++ b/terminusdb_client/client/__init__.py @@ -1 +1 @@ -from .Client import Patch, Client # noqa +from .Client import GraphType, Patch, Client # noqa diff --git a/terminusdb_client/schema/schema.py b/terminusdb_client/schema/schema.py index c278c001..4230bc8e 100644 --- a/terminusdb_client/schema/schema.py +++ b/terminusdb_client/schema/schema.py @@ -10,7 +10,7 @@ from typeguard import check_type from .. import woql_type as wt -from ..client.Client import Client +from ..client import Client, GraphType from ..woql_type import to_woql_type @@ -666,13 +666,13 @@ def commit( commit_msg = "Schema object insert/ update by Python client." if full_replace: client.insert_document( - self, commit_msg=commit_msg, graph_type="schema", full_replace=True + self, commit_msg=commit_msg, graph_type=GraphType.SCHEMA, full_replace=True ) else: client.update_document( self, commit_msg=commit_msg, - graph_type="schema", + graph_type=GraphType.SCHEMA, ) def from_db(self, client: Client, select: Optional[List[str]] = None): @@ -685,7 +685,7 @@ def from_db(self, client: Client, select: Optional[List[str]] = None): select: list of str, optional The classes (and depended classes) that will be imported, default to None which will import all classes """ - all_existing_class_raw = client.get_all_documents(graph_type="schema") + all_existing_class_raw = client.get_all_documents(graph_type=GraphType.SCHEMA) # clean up and update all_existing_classes for item in all_existing_class_raw: item_id = item.get("@id") diff --git a/terminusdb_client/tests/integration_tests/test_client.py b/terminusdb_client/tests/integration_tests/test_client.py index 3bd432ec..d7a07597 100644 --- a/terminusdb_client/tests/integration_tests/test_client.py +++ b/terminusdb_client/tests/integration_tests/test_client.py @@ -7,12 +7,23 @@ import pytest from terminusdb_client.errors import DatabaseError, InterfaceError -from terminusdb_client.client.Client import Patch, Client +from terminusdb_client import GraphType, Patch, Client from terminusdb_client.woqlquery.woql_query import WOQLQuery test_user_agent = "terminusdb-client-python-tests" +def test_not_ok(): + client = Client('http://localhost:6363') + with pytest.raises(Exception) as _: + client.ok() + + +def test_ok(docker_url): + client = Client(docker_url) + assert client.ok() + + def test_happy_path(docker_url): # create client client = Client(docker_url) @@ -117,6 +128,22 @@ def test_diff_object(docker_url): assert diff == {'test': {'@before': 'wew', '@after': 'wow', '@op': 'SwapValue'}} +def test_class_frame(docker_url): + client = Client(docker_url, user_agent=test_user_agent) + client.connect() + db_name = "philosophers" + str(random()) + client.create_database(db_name) + client.connect(db=db_name) + # Add a philosopher schema + 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'} + + def test_diff_apply_version(docker_url): client = Client(docker_url, user_agent=test_user_agent) client.connect() diff --git a/terminusdb_client/tests/test_Client.py b/terminusdb_client/tests/test_Client.py index 8c16317b..16da330c 100644 --- a/terminusdb_client/tests/test_Client.py +++ b/terminusdb_client/tests/test_Client.py @@ -7,7 +7,7 @@ import requests -from terminusdb_client.client import Client +from terminusdb_client.client import Client, GraphType from terminusdb_client.errors import InterfaceError from terminusdb_client.woqlschema import WOQLSchema @@ -200,6 +200,22 @@ def test_get_triples(mocked_requests, mocked_requests2): ) +@pytest.mark.skip(reason="temporary not avaliable") +@mock.patch("requests.head", side_effect=mocked_request_success) +@mock.patch("requests.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") + + client.get_triples(GraphType.INSTANCE) + + requests.get.assert_called_with( + "http://localhost:6363/api/triples/admin/myDBName/local/branch/main/instance", + auth=("admin", "root"), + headers={"user-agent": f"terminusdb-client-python/{__version__}"}, + ) + + @mock.patch("requests.post", side_effect=mocked_request_insert_delete) @mock.patch("requests.head", side_effect=mocked_request_success) @mock.patch("requests.get", side_effect=mocked_request_success) diff --git a/terminusdb_client/woql_utils.py b/terminusdb_client/woql_utils.py index 6d8ef7c5..348b9abe 100644 --- a/terminusdb_client/woql_utils.py +++ b/terminusdb_client/woql_utils.py @@ -20,6 +20,10 @@ def _result2stream(result): yield data +def _args_as_payload(args: dict) -> dict: + return {k: v for k, v in args.items() if v} + + def _finish_response(request_response, get_version=False): """Get the response text diff --git a/terminusdb_client/woqlclient/__init__.py b/terminusdb_client/woqlclient/__init__.py index 8be19ce1..fa2b79e4 100644 --- a/terminusdb_client/woqlclient/__init__.py +++ b/terminusdb_client/woqlclient/__init__.py @@ -1,4 +1,4 @@ import sys # noqa -from ..client import Patch, Client # noqa +from ..client import GraphType, Patch, Client # noqa WOQLClient = Client sys.modules["terminusdb_client.woqlclient.woqlClient"] = Client # noqa