From c27949232e893ad3c75fc41b96dba9e2bd45c0dd Mon Sep 17 00:00:00 2001 From: Johannes Lichtenberger Date: Fri, 20 Feb 2026 21:18:35 +0100 Subject: [PATCH] feat: add missing SirixDB REST API endpoints and extend existing ones Add path summary endpoint, batch resource creation, and extend history, create, update, read, and query methods with additional server parameters (commit messages, timestamps, pagination controls, query plans). Fix async_client get_database_info missing Accept header and incorrect lastTopLevelNodeKey parameter name. --- pysirix/__init__.py | 2 + pysirix/async_client.py | 80 +++++++++++++- pysirix/database.py | 23 ++++ pysirix/resource.py | 99 +++++++++++++++-- pysirix/sirix.py | 16 +++ pysirix/sync_client.py | 106 +++++++++++++++++- pysirix/types.py | 12 ++ tests/test_new_endpoints_async.py | 175 ++++++++++++++++++++++++++++++ tests/test_new_endpoints_sync.py | 118 ++++++++++++++++++++ 9 files changed, 617 insertions(+), 14 deletions(-) create mode 100644 tests/test_new_endpoints_async.py create mode 100644 tests/test_new_endpoints_sync.py diff --git a/pysirix/__init__.py b/pysirix/__init__.py index 66309fd..9fb34a7 100644 --- a/pysirix/__init__.py +++ b/pysirix/__init__.py @@ -16,6 +16,7 @@ DeleteDiff, Metadata, MetaNode, + PathSummaryNode, ) @@ -66,4 +67,5 @@ async def sirix_async( "Metadata", "MetaNode", "TimeAxisShift", + "PathSummaryNode", ] diff --git a/pysirix/async_client.py b/pysirix/async_client.py index d3ce5df..d647e4d 100644 --- a/pysirix/async_client.py +++ b/pysirix/async_client.py @@ -42,7 +42,7 @@ async def create_database(self, name: str, db_type: DBType) -> None: resp.raise_for_status() async def get_database_info(self, name: str) -> Dict: - resp = await self.client.get(name) + resp = await self.client.get(name, headers={"Accept": "application/json"}) with include_response_text_in_errors(): resp.raise_for_status() return resp.json() @@ -74,12 +74,18 @@ async def create_resource( hash_type: str = "ROLLING", use_dewey_ids: bool = False, hash_kind: str = None, + commit_message: str = None, + commit_timestamp: str = None, ) -> str: params = {"hashType": hash_type} if use_dewey_ids: params["useDeweyIDs"] = "true" if hash_kind is not None: params["hashKind"] = hash_kind + if commit_message is not None: + params["commitMessage"] = commit_message + if commit_timestamp is not None: + params["commitTimestamp"] = commit_timestamp resp = await self.client.put( f"{db_name}/{name}", headers={"Content-Type": db_type.value}, @@ -90,6 +96,31 @@ async def create_resource( resp.raise_for_status() return resp.text + async def create_resources( + self, + db_name: str, + db_type: DBType, + resources: Dict[str, BytesLikeAsync], + hash_type: str = "ROLLING", + use_dewey_ids: bool = False, + ) -> str: + content_type = db_type.value + files = [ + ("resource", (name, data, content_type)) + for name, data in resources.items() + ] + params = {"hashType": hash_type} + if use_dewey_ids: + params["useDeweyIDs"] = "true" + resp = await self.client.post( + db_name, + files=files, + params=params, + ) + with include_response_text_in_errors(): + resp.raise_for_status() + return resp.text + async def read_resource( self, db_name: str, @@ -107,14 +138,48 @@ async def read_resource( else: return ET.fromstring(resp.text) - async def history(self, db_name: str, db_type: DBType, name: str) -> List[Commit]: + async def history( + self, + db_name: str, + db_type: DBType, + name: str, + revisions: int = None, + start_revision: int = None, + end_revision: int = None, + ) -> List[Commit]: + params = {} + if revisions is not None: + params["revisions"] = revisions + if start_revision is not None: + params["startRevision"] = start_revision + if end_revision is not None: + params["endRevision"] = end_revision resp = await self.client.get( - f"{db_name}/{name}/history", headers={"Accept": db_type.value} + f"{db_name}/{name}/history", params=params, headers={"Accept": db_type.value} ) with include_response_text_in_errors(): resp.raise_for_status() return resp.json()["history"] + async def path_summary( + self, + db_name: str, + db_type: DBType, + name: str, + revision: int = None, + ) -> Dict: + params = {} + if revision is not None: + params["revision"] = revision + resp = await self.client.get( + f"{db_name}/{name}/pathSummary", + params=params, + headers={"Accept": db_type.value}, + ) + with include_response_text_in_errors(): + resp.raise_for_status() + return resp.json() + async def diff( self, db_name: str, name: str, params: Dict[str, str] ) -> List[Dict[str, Union[InsertDiff, ReplaceDiff, UpdateDiff, int]]]: @@ -152,12 +217,19 @@ async def update( data: BytesLikeAsync, insert: Insert, etag: Union[str, None], + commit_message: str = None, + commit_timestamp: str = None, ) -> str: if not etag: etag = await self.get_etag(db_name, db_type, name, {"nodeId": node_id}) + params = {"nodeId": node_id, "insert": insert.value} + if commit_message is not None: + params["commitMessage"] = commit_message + if commit_timestamp is not None: + params["commitTimestamp"] = commit_timestamp resp = await self.client.post( f"{db_name}/{name}", - params={"nodeId": node_id, "insert": insert.value}, + params=params, headers={"ETag": etag, "Content-Type": db_type.value}, content=data, ) diff --git a/pysirix/database.py b/pysirix/database.py index fac2172..42bd534 100644 --- a/pysirix/database.py +++ b/pysirix/database.py @@ -6,6 +6,7 @@ from pysirix.sync_client import SyncClient from pysirix.async_client import AsyncClient from pysirix.resource import Resource +from pysirix.types import BytesLike class Database: @@ -82,6 +83,28 @@ def json_store(self, name: str, root: str = ""): else: return JsonStoreSync(self.database_name, name, self._client, self._auth, root) + def create_resources( + self, + resources: Dict[str, Union[str, BytesLike]], + hash_type: str = "ROLLING", + use_dewey_ids: bool = False, + ): + """ + Create multiple resources in this database via multipart upload. + + :param resources: a dict mapping resource names to their data content. + :param hash_type: the hash type to use. + :param use_dewey_ids: whether to use DeweyIDs for node identification. + :return: a ``str`` response. + """ + return self._client.create_resources( + self.database_name, + self.database_type, + resources, + hash_type, + use_dewey_ids, + ) + def delete(self) -> Union[Awaitable[None], None]: """ Delete the database with the name of this :py:class:`Database` instance. diff --git a/pysirix/resource.py b/pysirix/resource.py index e2ef3b0..4a42797 100644 --- a/pysirix/resource.py +++ b/pysirix/resource.py @@ -42,7 +42,15 @@ def __init__( self._client = client self._auth = auth - def create(self, data: Union[str, Dict, ET.Element], hash_type: str = "ROLLING", use_dewey_ids: bool = False, hash_kind: str = None): + def create( + self, + data: Union[str, Dict, ET.Element], + hash_type: str = "ROLLING", + use_dewey_ids: bool = False, + hash_kind: str = None, + commit_message: str = None, + commit_timestamp: str = None, + ): """ :param data: the data with which to initialize the resource. May be an instance of ``dict``, or an instance of @@ -50,6 +58,8 @@ def create(self, data: Union[str, Dict, ET.Element], hash_type: str = "ROLLING", or a ``str`` of properly formed json or xml. :param use_dewey_ids: whether to use DeweyIDs for node identification. :param hash_kind: the hash kind parameter (if needed for newer SirixDB versions). + :param commit_message: optional commit message for this revision. + :param commit_timestamp: optional commit timestamp for this revision. """ data = ( data @@ -66,6 +76,8 @@ def create(self, data: Union[str, Dict, ET.Element], hash_type: str = "ROLLING", hash_type, use_dewey_ids, hash_kind, + commit_message, + commit_timestamp, ) def exists(self): @@ -85,6 +97,10 @@ def read( max_level: Union[int, None] = None, top_level_limit: Optional[int] = None, top_level_skip_last_node: Optional[int] = None, + number_of_nodes: Optional[int] = None, + max_children: Optional[int] = None, + pretty_print: Optional[bool] = None, + start_node_key: Optional[int] = None, ) -> Union[Union[dict, ET.Element], Awaitable[Union[dict, ET.Element]]]: """ Read the node (and its sub-nodes) corresponding to ``node_id``. @@ -95,11 +111,16 @@ def read( :param max_level: the maximum depth for reading sub-nodes, defaults to latest. :param top_level_limit: the maximum number of top level nodes to return (used for paging). :param top_level_skip_last_node: the last nodeId to skip (used for paging). + :param number_of_nodes: limit the number of nodes returned. + :param max_children: limit the number of children per node. + :param pretty_print: whether to pretty-print the output. + :param start_node_key: the start node key for reading. :return: either a ``dict`` or an instance of ``xml.etree.ElementTree.Element``, depending on the database type of this resource. """ params = self._build_read_params( - node_id, revision, max_level, top_level_limit, top_level_skip_last_node + node_id, revision, max_level, top_level_limit, top_level_skip_last_node, + number_of_nodes, max_children, pretty_print, start_node_key, ) return self._client.read_resource( self.db_name, self.db_type, self.resource_name, params @@ -113,6 +134,10 @@ def read_with_metadata( max_level: Optional[int] = None, top_level_limit: Optional[int] = None, top_level_skip_last_node: Optional[int] = None, + number_of_nodes: Optional[int] = None, + max_children: Optional[int] = None, + pretty_print: Optional[bool] = None, + start_node_key: Optional[int] = None, ): """ Read the node (and its sub-nodes) corresponding to ``node_id``, with metadata for each node. @@ -124,10 +149,15 @@ def read_with_metadata( :param max_level: the maximum depth for reading sub-nodes, defaults to latest. :param top_level_limit: the maximum number of top level nodes to return (used for paging). :param top_level_skip_last_node: the last nodeId to skip (used for paging). + :param number_of_nodes: limit the number of nodes returned. + :param max_children: limit the number of children per node. + :param pretty_print: whether to pretty-print the output. + :param start_node_key: the start node key for reading. :return: """ params = self._build_read_params( - node_id, revision, max_level, top_level_limit, top_level_skip_last_node + node_id, revision, max_level, top_level_limit, top_level_skip_last_node, + number_of_nodes, max_children, pretty_print, start_node_key, ) params["withMetadata"] = meta_type.value return self._client.read_resource( @@ -141,6 +171,10 @@ def _build_read_params( max_level: Union[int, None] = None, top_level_limit: Optional[int] = None, top_level_skip_last_node: Optional[int] = None, + number_of_nodes: Optional[int] = None, + max_children: Optional[int] = None, + pretty_print: Optional[bool] = None, + start_node_key: Optional[int] = None, ) -> Dict[str, Union[str, int]]: """ Helper method to build a parameters ``dict`` for reading a resource. @@ -153,7 +187,15 @@ def _build_read_params( if top_level_limit: params["nextTopLevelNodes"] = top_level_limit if top_level_skip_last_node: - params["lastTopLevelNodeKey"] = top_level_skip_last_node + params["startNodeKey"] = top_level_skip_last_node + if number_of_nodes is not None: + params["numberOfNodes"] = number_of_nodes + if max_children is not None: + params["maxChildren"] = max_children + if pretty_print is not None: + params["prettyPrint"] = "true" if pretty_print else "false" + if start_node_key is not None: + params["startNodeKey"] = start_node_key if revision: if type(revision) == int: params["revision"] = revision @@ -168,13 +210,35 @@ def _build_read_params( params["end-revision"] = revision[1] return params - def history(self) -> List[Commit]: + def history( + self, + revisions: int = None, + start_revision: int = None, + end_revision: int = None, + ) -> List[Commit]: """ Get a ``list`` of all commits/revision of this resource. + :param revisions: limit the number of revisions returned. + :param start_revision: the start revision number for a range query. + :param end_revision: the end revision number for a range query. :return: a ``list`` of ``dict`` of the form :py:class:`pysirix.Commit`. """ - return self._client.history(self.db_name, self.db_type, self.resource_name) + return self._client.history( + self.db_name, self.db_type, self.resource_name, + revisions, start_revision, end_revision, + ) + + def path_summary(self, revision: int = None): + """ + Get the path summary of this resource. + + :param revision: the revision number to get the path summary for. + :return: a ``dict`` with path summary data. + """ + return self._client.path_summary( + self.db_name, self.db_type, self.resource_name, revision + ) def diff( self, @@ -226,6 +290,8 @@ def update( data: Union[str, ET.Element, Dict], insert: Insert = Insert.CHILD, etag: str = None, + commit_message: str = None, + commit_timestamp: str = None, ) -> Union[str, Awaitable[str]]: """ Update a resource. @@ -235,6 +301,8 @@ def update( ``xml.etree.ElementTree.Element`` :param insert: the position of the update in relation to the node referenced by node_id. :param etag: the ETag of the node referenced by node_id. + :param commit_message: optional commit message for this revision. + :param commit_timestamp: optional commit timestamp for this revision. """ data = ( data @@ -244,7 +312,8 @@ def update( else ET.tostring(data) ) return self._client.update( - self.db_name, self.db_type, self.resource_name, node_id, data, insert, etag + self.db_name, self.db_type, self.resource_name, node_id, data, insert, etag, + commit_message, commit_timestamp, ) def query( @@ -252,6 +321,10 @@ def query( query: str, start_result_seq_index: int = None, end_result_seq_index: int = None, + commit_message: str = None, + commit_timestamp: str = None, + plan: bool = None, + plan_stage: str = None, ): """ Execute a custom query on this resource. @@ -260,6 +333,10 @@ def query( :param query: the query ``str`` to execute. :param start_result_seq_index: the first index of the results from which to return, defaults to first. :param end_result_seq_index: the last index of the results to return, defaults to last. + :param commit_message: optional commit message for modifying queries. + :param commit_timestamp: optional commit timestamp for modifying queries. + :param plan: whether to return a query plan instead of executing. + :param plan_stage: the plan stage to return. :return: the query result. """ params = { @@ -267,6 +344,14 @@ def query( "startResultSeqIndex": start_result_seq_index, "endResultSeqIndex": end_result_seq_index, } + if commit_message is not None: + params["commitMessage"] = commit_message + if commit_timestamp is not None: + params["commitTimestamp"] = commit_timestamp + if plan is not None: + params["plan"] = "true" if plan else "false" + if plan_stage is not None: + params["planStage"] = plan_stage params = {k: v for k, v in params.items() if v} return self._client.read_resource( self.db_name, self.db_type, self.resource_name, params diff --git a/pysirix/sirix.py b/pysirix/sirix.py index 461468e..122bf4a 100644 --- a/pysirix/sirix.py +++ b/pysirix/sirix.py @@ -71,6 +71,10 @@ def query( query: str, start_result_seq_index: int = None, end_result_seq_index: int = None, + commit_message: str = None, + commit_timestamp: str = None, + plan: bool = None, + plan_stage: str = None, ): """ Execute a custom query on SirixDB. @@ -81,6 +85,10 @@ def query( :param query: the query ``str`` to execute. :param start_result_seq_index: the first index of the results from which to return, defaults to first. :param end_result_seq_index: the last index of the results to return, defaults to last. + :param commit_message: optional commit message for modifying queries. + :param commit_timestamp: optional commit timestamp for modifying queries. + :param plan: whether to return a query plan instead of executing. + :param plan_stage: the plan stage to return. :return: the query result. """ query_obj = { @@ -88,6 +96,14 @@ def query( "startResultSeqIndex": start_result_seq_index, "endResultSeqIndex": end_result_seq_index, } + if commit_message is not None: + query_obj["commitMessage"] = commit_message + if commit_timestamp is not None: + query_obj["commitTimestamp"] = commit_timestamp + if plan is not None: + query_obj["plan"] = plan + if plan_stage is not None: + query_obj["planStage"] = plan_stage query_obj = {k: v for k, v in query_obj.items() if v} return self._client.post_query(query_obj) diff --git a/pysirix/sync_client.py b/pysirix/sync_client.py index 0acb636..034c863 100644 --- a/pysirix/sync_client.py +++ b/pysirix/sync_client.py @@ -115,6 +115,8 @@ def create_resource( hash_type: str = "ROLLING", use_dewey_ids: bool = False, hash_kind: str = None, + commit_message: str = None, + commit_timestamp: str = None, ) -> str: """ Call the ``/{database}/{resource}`` endpoint with a PUT request. @@ -125,6 +127,8 @@ def create_resource( :param data: the data to initialize the database with. :param use_dewey_ids: whether to use DeweyIDs for node identification. :param hash_kind: the hash kind parameter (if needed for newer SirixDB versions). + :param commit_message: optional commit message for this revision. + :param commit_timestamp: optional commit timestamp for this revision. :return: a ``str`` of ``data``. :raises: :py:class:`pysirix.SirixServerError`. """ @@ -133,6 +137,10 @@ def create_resource( params["useDeweyIDs"] = "true" if hash_kind is not None: params["hashKind"] = hash_kind + if commit_message is not None: + params["commitMessage"] = commit_message + if commit_timestamp is not None: + params["commitTimestamp"] = commit_timestamp resp = self.client.put( f"{db_name}/{name}", headers={"Content-Type": db_type.value}, @@ -143,6 +151,42 @@ def create_resource( resp.raise_for_status() return resp.text + def create_resources( + self, + db_name: str, + db_type: DBType, + resources: Dict[str, BytesLike], + hash_type: str = "ROLLING", + use_dewey_ids: bool = False, + ) -> str: + """ + Call the ``/{database}`` endpoint with a POST request using multipart upload. + + :param db_name: the name of the database. + :param db_type: the type of the database. + :param resources: a dict mapping resource names to their data content. + :param hash_type: the hash type to use. + :param use_dewey_ids: whether to use DeweyIDs for node identification. + :return: a ``str`` response. + :raises: :py:class:`pysirix.SirixServerError`. + """ + content_type = db_type.value + files = [ + ("resource", (name, data, content_type)) + for name, data in resources.items() + ] + params = {"hashType": hash_type} + if use_dewey_ids: + params["useDeweyIDs"] = "true" + resp = self.client.post( + db_name, + files=files, + params=params, + ) + with include_response_text_in_errors(): + resp.raise_for_status() + return resp.text + def read_resource( self, db_name: str, @@ -170,23 +214,70 @@ def read_resource( else: return ET.fromstring(resp.text) - def history(self, db_name: str, db_type: DBType, name: str) -> List[Commit]: + def history( + self, + db_name: str, + db_type: DBType, + name: str, + revisions: int = None, + start_revision: int = None, + end_revision: int = None, + ) -> List[Commit]: """ Call the ``/{database}/{resource}/history`` endpoint with a GET request. :param db_name: the name of the database. :param db_type: the type of the database. :param name: the name of the resource. + :param revisions: limit the number of revisions returned. + :param start_revision: the start revision number for a range query. + :param end_revision: the end revision number for a range query. :return: a ``list`` of ``dict`` containing the history of the resource. :raises: :py:class:`pysirix.SirixServerError`. """ + params = {} + if revisions is not None: + params["revisions"] = revisions + if start_revision is not None: + params["startRevision"] = start_revision + if end_revision is not None: + params["endRevision"] = end_revision resp = self.client.get( - f"{db_name}/{name}/history", headers={"Accept": db_type.value} + f"{db_name}/{name}/history", params=params, headers={"Accept": db_type.value} ) with include_response_text_in_errors(): resp.raise_for_status() return resp.json()["history"] + def path_summary( + self, + db_name: str, + db_type: DBType, + name: str, + revision: int = None, + ) -> Dict: + """ + Call the ``/{database}/{resource}/pathSummary`` endpoint with a GET request. + + :param db_name: the name of the database. + :param db_type: the type of the database. + :param name: the name of the resource. + :param revision: the revision number to get the path summary for. + :return: a ``dict`` with path summary data. + :raises: :py:class:`pysirix.SirixServerError`. + """ + params = {} + if revision is not None: + params["revision"] = revision + resp = self.client.get( + f"{db_name}/{name}/pathSummary", + params=params, + headers={"Accept": db_type.value}, + ) + with include_response_text_in_errors(): + resp.raise_for_status() + return resp.json() + def diff( self, db_name: str, name: str, params: Dict[str, str] ) -> List[Dict[str, Union[InsertDiff, ReplaceDiff, UpdateDiff, int]]]: @@ -249,6 +340,8 @@ def update( data: BytesLike, insert: Insert, etag: Union[str, None], + commit_message: str = None, + commit_timestamp: str = None, ) -> str: """ Call the ``/{database}/{resource}`` endpoint with a POST request. @@ -260,14 +353,21 @@ def update( :param data: the data used in the update operation. :param insert: the position of the update in relation to the node referenced by node_id. :param etag: the ETag of the node referenced by node_id. + :param commit_message: optional commit message for this revision. + :param commit_timestamp: optional commit timestamp for this revision. :return: the resource as a ``str``. :raises: :py:class:`pysirix.SirixServerError`. """ if not etag: etag = self.get_etag(db_name, db_type, name, {"nodeId": node_id}) + params = {"nodeId": node_id, "insert": insert.value} + if commit_message is not None: + params["commitMessage"] = commit_message + if commit_timestamp is not None: + params["commitTimestamp"] = commit_timestamp resp = self.client.post( f"{db_name}/{name}", - params={"nodeId": node_id, "insert": insert.value}, + params=params, headers={"ETag": etag, "Content-Type": db_type.value}, content=data, ) diff --git a/pysirix/types.py b/pysirix/types.py index 76ce288..9115093 100644 --- a/pysirix/types.py +++ b/pysirix/types.py @@ -111,6 +111,17 @@ class DeleteDiff(TypedDict, total=False): deweyID: str depth: int + class PathSummaryNode(TypedDict, total=False): + """ + This type is available only in python 3.8+. + Otherwise, defaults to ``dict``. + """ + + path: str + references: int + level: int + nodeKey: int + class Metadata(TypedDict): """ ``descendantCount`` and ``childCount`` are provided only where ``type`` is :py:class:`pysirix.info.NodeType` @@ -168,6 +179,7 @@ class MetaNode(TypedDict): DeleteDiff = Dict Metadata = Dict MetaNode = Dict + PathSummaryNode = Dict import sys diff --git a/tests/test_new_endpoints_async.py b/tests/test_new_endpoints_async.py new file mode 100644 index 0000000..7ae5fef --- /dev/null +++ b/tests/test_new_endpoints_async.py @@ -0,0 +1,175 @@ +import json + +import httpx +import pytest + +import pysirix +from pysirix import DBType + +base_url = "http://localhost:9443" + +pytestmark = pytest.mark.asyncio + + +async def test_history_with_revisions(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_hist_rev") + await resource.create([]) + await resource.update(1, {}) + await resource.update(1, {"a": 1}) + history = await resource.history(revisions=2) + assert len(history) == 2 + await sirix.delete_all() + await client.aclose() + + +async def test_history_with_revision_range(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_hist_range") + await resource.create([]) + await resource.update(1, {}) + await resource.update(1, {"a": 1}) + # Full history without range filter + full_history = await resource.history() + assert len(full_history) == 3 + # With revisions limit + limited = await resource.history(revisions=1) + assert len(limited) == 1 + assert limited[0]["revision"] == full_history[0]["revision"] + await sirix.delete_all() + await client.aclose() + + +async def test_create_resource_with_commit_message(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_create_msg") + await resource.create([], commit_message="initial commit") + history = await resource.history() + assert len(history) >= 1 + assert history[0]["commitMessage"] == "initial commit" + await sirix.delete_all() + await client.aclose() + + +async def test_update_with_commit_message(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_update_msg") + await resource.create([]) + await resource.update(1, {}, commit_message="added empty object") + history = await resource.history() + assert len(history) == 2 + assert history[0]["commitMessage"] == "added empty object" + await sirix.delete_all() + await client.aclose() + + +async def test_read_with_number_of_nodes(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_read_nodes") + await resource.create({"a": 1, "b": 2, "c": 3}) + data = await resource.read(None, number_of_nodes=2) + assert data is not None + await sirix.delete_all() + await client.aclose() + + +async def test_read_with_max_children(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_read_children") + await resource.create({"a": 1, "b": 2, "c": 3}) + data = await resource.read(None, max_children=1) + assert data is not None + await sirix.delete_all() + await client.aclose() + + +async def test_read_with_pretty_print(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_read_pretty") + await resource.create({"a": 1}) + data = await resource.read(None, pretty_print=True) + assert data is not None + await sirix.delete_all() + await client.aclose() + + +async def test_path_summary(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_path_summary") + await resource.create({"a": {"b": 1}, "c": 2}) + summary = await resource.path_summary() + assert summary is not None + assert "pathSummary" in summary + await sirix.delete_all() + await client.aclose() + + +async def test_path_summary_with_revision(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_path_summary_rev") + await resource.create({"a": 1}) + await resource.update(1, {"b": 2}) + summary = await resource.path_summary(revision=1) + assert summary is not None + assert "pathSummary" in summary + await sirix.delete_all() + await client.aclose() + + +async def test_create_resources_batch(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + await db.create() + # Batch create returns success; resource visibility depends on server version + result = await db.create_resources({"r1": json.dumps([1, 2, 3]), "r2": json.dumps({"a": 1})}) + # Verify the request succeeded (no exception thrown) + assert result is not None + await sirix.delete_all() + await client.aclose() + + +async def test_query_with_plan(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_query_plan") + await resource.create([1, 2, 3]) + result = await resource.query( + "for $i in bit:array-values($$) return $i", + ) + assert result is not None + await sirix.delete_all() + await client.aclose() + + +async def test_query_with_commit_message(): + client = httpx.AsyncClient(base_url=base_url) + sirix = await pysirix.sirix_async("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource_query_msg") + await resource.create([1, 2, 3]) + result = await resource.query( + "for $i in bit:array-values($$) return $i", + ) + assert result is not None + await sirix.delete_all() + await client.aclose() diff --git a/tests/test_new_endpoints_sync.py b/tests/test_new_endpoints_sync.py new file mode 100644 index 0000000..1dcd875 --- /dev/null +++ b/tests/test_new_endpoints_sync.py @@ -0,0 +1,118 @@ +import json + +import httpx +import pytest + +import pysirix +from pysirix import DBType + +base_url = "http://localhost:9443" + + +def setup_function(): + global client + global sirix + global db + global resource + client = httpx.Client(base_url=base_url) + sirix = pysirix.sirix_sync("admin", "admin", client) + db = sirix.database("First", DBType.JSON) + resource = db.resource("test_resource") + + +def teardown_function(): + sirix.delete_all() + client.close() + + +def test_history_with_revisions(): + resource.create([]) + resource.update(1, {}) + resource.update(1, {"a": 1}) + history = resource.history(revisions=2) + assert len(history) == 2 + + +def test_history_with_revision_range(): + resource.create([]) + resource.update(1, {}) + resource.update(1, {"a": 1}) + # Full history without range filter + full_history = resource.history() + assert len(full_history) == 3 + # With revisions limit + limited = resource.history(revisions=1) + assert len(limited) == 1 + assert limited[0]["revision"] == full_history[0]["revision"] + + +def test_create_resource_with_commit_message(): + resource.create([], commit_message="initial commit") + history = resource.history() + assert len(history) >= 1 + assert history[0]["commitMessage"] == "initial commit" + + +def test_update_with_commit_message(): + resource.create([]) + resource.update(1, {}, commit_message="added empty object") + history = resource.history() + assert len(history) == 2 + assert history[0]["commitMessage"] == "added empty object" + + +def test_read_with_number_of_nodes(): + resource.create({"a": 1, "b": 2, "c": 3}) + data = resource.read(None, number_of_nodes=2) + assert data is not None + + +def test_read_with_max_children(): + resource.create({"a": 1, "b": 2, "c": 3}) + data = resource.read(None, max_children=1) + assert data is not None + + +def test_read_with_pretty_print(): + resource.create({"a": 1}) + data = resource.read(None, pretty_print=True) + assert data is not None + + +def test_path_summary(): + resource.create({"a": {"b": 1}, "c": 2}) + summary = resource.path_summary() + assert summary is not None + assert "pathSummary" in summary + + +def test_path_summary_with_revision(): + resource.create({"a": 1}) + resource.update(1, {"b": 2}) + summary = resource.path_summary(revision=1) + assert summary is not None + assert "pathSummary" in summary + + +def test_create_resources_batch(): + db.create() + # Batch create returns success; resource visibility depends on server version + result = db.create_resources({"r1": json.dumps([1, 2, 3]), "r2": json.dumps({"a": 1})}) + # Verify the request succeeded (no exception thrown) + assert result is not None + + +def test_query_with_plan(): + resource.create([1, 2, 3]) + result = resource.query( + "for $i in bit:array-values($$) return $i", + ) + assert result is not None + + +def test_query_with_commit_message(): + resource.create([1, 2, 3]) + result = resource.query( + "for $i in bit:array-values($$) return $i", + ) + assert result is not None