From ddff1595202a7dc46bf61ce2845ed66ac423c163 Mon Sep 17 00:00:00 2001 From: Shubra Gadhwala Date: Tue, 2 Dec 2025 19:20:16 +0530 Subject: [PATCH] Implemented methods for retrieving all tenant credentials, edges, graphs, labels, nodes, tags, and vectors, as well as methods for deleting all tenant resources across these categories. Updated demo files to showcase these new functionalities. Enhanced mixins to support retrieval and deletion operations at the tenant level, improving overall API resource management. --- examples/demoCredential.py | 20 +- examples/demoEdge.py | 60 +- examples/demoGraphs.py | 20 +- examples/demoLabel.py | 114 +++- examples/demoNode.py | 53 ++ examples/demoTags.py | 103 +++- examples/demoVector.py | 98 ++- src/litegraph/mixins.py | 566 ++++++++++++++++- src/litegraph/models/node.py | 3 + src/litegraph/resources/credentials.py | 72 +++ src/litegraph/resources/edges.py | 107 ++++ src/litegraph/resources/graphs.py | 26 + src/litegraph/resources/labels.py | 164 +++++ src/litegraph/resources/nodes.py | 97 +++ src/litegraph/resources/tags.py | 125 ++++ src/litegraph/resources/vectors.py | 180 ++++++ tests/conftest.py | 18 +- tests/test_mixins.py | 814 ++++++++++++++++++++++--- tests/test_models/test_tags.py | 254 ++++++++ tests/test_models/test_vector_index.py | 345 +++++++++++ tests/test_models/test_vectors.py | 353 ++++++++++- 21 files changed, 3485 insertions(+), 107 deletions(-) create mode 100644 tests/test_models/test_tags.py create mode 100644 tests/test_models/test_vector_index.py diff --git a/examples/demoCredential.py b/examples/demoCredential.py index f2b152f..9509407 100644 --- a/examples/demoCredential.py +++ b/examples/demoCredential.py @@ -98,4 +98,22 @@ def exists_credential(): print(exists) -exists_credential() +# exists_credential() + + +def delete_all_tenant_credential(): + litegraph.Credential.delete_all_tenant_credentials( + tenant_guid="00000000-0000-0000-0000-000000000000" + ) + print("All tenant credentials deleted") + + +# delete_all_tenant_credential() + + +def retrieve_credential_by_bearer_token(): + credential = litegraph.Credential.get_bearer_credentials(bearer_token="foobar") + print(credential) + + +retrieve_credential_by_bearer_token() diff --git a/examples/demoEdge.py b/examples/demoEdge.py index 1773056..7de53c8 100644 --- a/examples/demoEdge.py +++ b/examples/demoEdge.py @@ -146,4 +146,62 @@ def retrieve_first_edge(): print(graph) -retrieve_first_edge() +# retrieve_first_edge() + + +def get_all_graph_edges(): + edges = litegraph.Edge.retrieve_all_graph_edges( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print(edges) + + +get_all_graph_edges() + + +def get_all_tenant_edges(): + edges = litegraph.Edge.retrieve_all_tenant_edges( + tenant_guid="00000000-0000-0000-0000-000000000000", + ) + print(edges) + + +get_all_tenant_edges() + + +def delete_all_tenant_edges(): + litegraph.Edge.delete_all_tenant_edges( + tenant_guid="00000000-0000-0000-0000-000000000000", + ) + print("Edges deleted") + + +# delete_all_tenant_edges() + + +def delete_node_edges(): + litegraph.Edge.delete_node_edges( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + node_guid="51c6bb09-76a5-45ce-b4a4-dcc902a383d3", + ) + print("Edges deleted") + + +# delete_node_edges() + + +def delete_node_edges_bulk(): + litegraph.Edge.delete_node_edges_bulk( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + node_guids=[ + "51c6bb09-76a5-45ce-b4a4-dcc902a383d3", + "51c6bb09-76a5-45ce-b4a4-dcc902a383d3", + ], + ) + print("Edges deleted") + + +# delete_node_edges_bulk() diff --git a/examples/demoGraphs.py b/examples/demoGraphs.py index dc495a7..55b496b 100644 --- a/examples/demoGraphs.py +++ b/examples/demoGraphs.py @@ -167,7 +167,7 @@ def retrieve_subgraph(): print(subgraph) -retrieve_subgraph() +# retrieve_subgraph() def retrieve_subgraph_statistics(): @@ -178,4 +178,20 @@ def retrieve_subgraph_statistics(): print(statistics) -retrieve_subgraph_statistics() +# retrieve_subgraph_statistics() + + +def retrieve_all_tenant_graphs(): + graphs = litegraph.Graph.retrieve_all_tenant_graphs() + print(graphs) + + +# retrieve_all_tenant_graphs() + + +def delete_all_tenant_graphs(): + litegraph.Graph.delete_all_tenant_graphs() + print("All tenant graphs deleted") + + +# delete_all_tenant_graphs() diff --git a/examples/demoLabel.py b/examples/demoLabel.py index 4840674..0bce910 100644 --- a/examples/demoLabel.py +++ b/examples/demoLabel.py @@ -3,6 +3,7 @@ sdk = litegraph.configure( endpoint="http://YOUR_SERVER_URL_HERE:PORT", tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", access_key="litegraphadmin", ) @@ -66,7 +67,7 @@ def enumerate_with_query_label(): print(labels) -enumerate_with_query_label() +# enumerate_with_query_label() def update_label(): @@ -111,4 +112,113 @@ def exists_label(): print(exists) -exists_label() +# exists_label() + + +def get_all_tenant_labels(): + labels = litegraph.Label.retrieve_all_tenant_labels( + tenant_guid="00000000-0000-0000-0000-000000000000" + ) + print(labels) + + +# get_all_tenant_labels() + + +def get_all_graph_labels(): + labels = litegraph.Label.retrieve_all_graph_labels( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print(labels) + + +# get_all_graph_labels() + + +def get_node_labels(): + labels = litegraph.Label.retrieve_node_labels( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + node_guid="bd74d996-4a2d-48e0-9e93-110d19dd7fb2", + ) + print(labels) + + +# get_node_labels() + + +def get_edge_labels(): + labels = litegraph.Label.retrieve_edge_labels( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + edge_guid="4015a4e1-b744-4727-ab7e-cd0fbc7fd8b8", + ) + print(labels) + + +# get_edge_labels() + + +def delete_all_tenant_labels(): + litegraph.Label.delete_all_tenant_labels( + tenant_guid="00000000-0000-0000-0000-000000000000" + ) + print("All tenant labels deleted") + + +delete_all_tenant_labels() + + +def delete_all_graph_labels(): + litegraph.Label.delete_all_graph_labels( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print("All graph labels deleted") + + +delete_all_graph_labels() + + +def delete_graph_labels(): + litegraph.Label.delete_graph_labels( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print("Graph labels deleted") + + +delete_graph_labels() + + +def delete_node_labels(): + litegraph.Label.delete_node_labels( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + node_guid="17155e85-9c9d-481e-a4e2-14d386fbe225", + ) + print("Node labels deleted") + + +# delete_node_labels() + + +def delete_edge_labels(): + litegraph.Label.delete_edge_labels( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + edge_guid="dbafd244-bd7d-4668-bf1d-ca773d93058b", + ) + print("Edge labels deleted") + + +# delete_edge_labels() + + +def get_graph_labels(): + labels = litegraph.Label.retrieve_graph_labels() + print(labels) + + +# get_graph_labels() diff --git a/examples/demoNode.py b/examples/demoNode.py index b8ec57f..91d25c7 100644 --- a/examples/demoNode.py +++ b/examples/demoNode.py @@ -143,3 +143,56 @@ def delete_all_node(): # delete_all_node() + + +def delete_all_tenant_nodes(): + litegraph.Node.delete_all_tenant_nodes( + tenant_guid="00000000-0000-0000-0000-000000000000" + ) + print("All tenant nodes deleted") + + +# delete_all_tenant_nodes() + + +def retrieve_all_tenant_nodes(): + nodes = litegraph.Node.retrieve_all_tenant_nodes( + tenant_guid="00000000-0000-0000-0000-000000000000" + ) + print(nodes) + + +# retrieve_all_tenant_nodes() + + +def retrieve_all_graph_nodes(): + nodes = litegraph.Node.retrieve_all_graph_nodes( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print(nodes) + + +# retrieve_all_graph_nodes() + + +def retrieve_most_connected_nodes(): + nodes = litegraph.Node.retrieve_most_connected_nodes( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print(nodes) + + +# retrieve_most_connected_nodes() + + +def retrieve_least_connected_nodes(): + nodes = litegraph.Node.retrieve_least_connected_nodes( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print(nodes) + + +# retrieve_least_connected_nodes() diff --git a/examples/demoTags.py b/examples/demoTags.py index dcb2f4c..f0e34b2 100644 --- a/examples/demoTags.py +++ b/examples/demoTags.py @@ -118,4 +118,105 @@ def exists_tag(): print(exists) -exists_tag() +# exists_tag() + + +def retrieve_all_tenant_tags(): + tags = litegraph.Tag.retrieve_all_tenant_tags( + tenant_guid="00000000-0000-0000-0000-000000000000" + ) + print(tags) + + +# retrieve_all_tenant_tags() + + +def retrieve_all_graph_tags(): + tags = litegraph.Tag.retrieve_all_graph_tags( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print(tags) + + +# retrieve_all_graph_tags() + + +def retrieve_node_tags(): + tags = litegraph.Tag.retrieve_node_tags( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + node_guid="00000000-0000-0000-0000-000000000000", + ) + print(tags) + + +# retrieve_node_tags() + + +def retrieve_edge_tags(): + tags = litegraph.Tag.retrieve_edge_tags( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + edge_guid="a774551f-3c55-4a13-a23f-7213fecadc86", + ) + print(tags) + + +# retrieve_edge_tags() + + +def delete_all_tenant_tags(): + litegraph.Tag.delete_all_tenant_tags( + tenant_guid="00000000-0000-0000-0000-000000000000" + ) + print("All tenant tags deleted") + + +# delete_all_tenant_tags() + + +def delete_all_graph_tags(): + litegraph.Tag.delete_all_graph_tags( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print("All graph tags deleted") + + +# delete_all_graph_tags() + + +def delete_graph_tags(): + litegraph.Tag.delete_graph_tags( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print("Graph tags deleted") + + +# delete_graph_tags() + + +def delete_node_tags(): + litegraph.Tag.delete_node_tags( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + node_guid="00000000-0000-0000-0000-000000000000", + ) + print("Node tags deleted") + + +# delete_node_tags() + + +def delete_edge_tags(): + litegraph.Tag.delete_edge_tags( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + edge_guid="a774551f-3c55-4a13-a23f-7213fecadc86", + ) + print("Edge tags deleted") + + +# delete_edge_tags() diff --git a/examples/demoVector.py b/examples/demoVector.py index fc0a0dc..5e383f7 100644 --- a/examples/demoVector.py +++ b/examples/demoVector.py @@ -3,6 +3,7 @@ sdk = litegraph.configure( endpoint="http://YOUR_SERVER_URL_HERE:PORT", tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", access_key="litegraphadmin", ) @@ -70,7 +71,7 @@ def update_vector(): print(vector) -update_vector() +# update_vector() def delete_vector(): @@ -101,7 +102,7 @@ def enumerate_with_query_vector(): print(vectors) -enumerate_with_query_vector() +# enumerate_with_query_vector() def create_multiple_vector(): @@ -130,7 +131,7 @@ def create_multiple_vector(): print(vectors) -create_multiple_vector() +# create_multiple_vector() def delete_multiple_vector(): @@ -141,3 +142,94 @@ def delete_multiple_vector(): # delete_multiple_vector() + + +def delete_all_tenant_vectors(): + litegraph.Vector.delete_all_tenant_vectors() + print("All tenant vectors deleted") + + +# delete_all_tenant_vectors() + + +def delete_all_graph_vectors(): + litegraph.Vector.delete_all_graph_vectors( + tenant_guid="00000000-0000-0000-0000-000000000000", + graph_guid="00000000-0000-0000-0000-000000000000", + ) + print("All graph vectors deleted") + + +# delete_all_graph_vectors() + + +def retrieve_all_tenant_vectors(): + vectors = litegraph.Vector.retrieve_all_tenant_vectors() + print(vectors) + + +# retrieve_all_tenant_vectors() + + +def retrieve_all_graph_vectors(): + vectors = litegraph.Vector.retrieve_all_graph_vectors() + print(vectors) + + +# retrieve_all_graph_vectors() + + +def retrieve_node_vectors(): + vectors = litegraph.Vector.retrieve_node_vectors( + node_guid="b2eee912-31fe-4ca5-9807-214e9ceebcc3", + ) + print(vectors) + + +# retrieve_node_vectors() + + +def retrieve_edge_vectors(): + vectors = litegraph.Vector.retrieve_edge_vectors( + edge_guid="00000000-0000-0000-0000-000000000000", + ) + print(vectors) + + +# retrieve_edge_vectors() + + +def retrieve_graph_vectors(): + vectors = litegraph.Vector.retrieve_graph_vectors() + print(vectors) + + +# retrieve_graph_vectors() + + +def delete_graph_vectors(): + litegraph.Vector.delete_graph_vectors() + print("Graph vectors deleted") + + +# delete_graph_vectors() + + +def delete_node_vectors(): + litegraph.Vector.delete_node_vectors( + node_guid="b2eee912-31fe-4ca5-9807-214e9ceebcc3", + ) + print("Node vectors deleted") + + +# delete_node_vectors() + + +def delete_edge_vectors(): + litegraph.Vector.delete_edge_vectors( + edge_guid="00000000-0000-0000-0000-000000000000", + ) + print("Edge vectors deleted") + + +delete_edge_vectors() diff --git a/src/litegraph/mixins.py b/src/litegraph/mixins.py index f9d2a75..c509d36 100755 --- a/src/litegraph/mixins.py +++ b/src/litegraph/mixins.py @@ -10,7 +10,7 @@ from .models.enumeration_query import EnumerationQueryModel from .models.enumeration_result import EnumerationResultModel from .sdk_logging import log_error -from .utils.url_helper import _get_url_v1, _get_url_v2 +from .utils.url_helper import _get_url_base, _get_url_v1, _get_url_v2 JSON_CONTENT_TYPE = {"Content-Type": "application/json"} @@ -676,3 +676,567 @@ def retrieve_many( if cls.MODEL else instance ) + + +class RetrievableAllEndpointMixin: + """ + Mixin class for retrieving all resources using the /all endpoint. + Provides methods for both tenant-level and graph-level retrieval. + """ + + RESOURCE_NAME: str = "" + MODEL: Optional[Type[BaseModel]] = None + REQUIRE_TENANT: bool = True + REQUIRE_GRAPH_GUID: bool = True + + @classmethod + def retrieve_all_tenant(cls, tenant_guid: str | None = None) -> list["BaseModel"]: + """ + Retrieve all resources for a tenant using the /all endpoint. + + Endpoint: + /v1.0/tenants/{tenant}/{resource_name}/all + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + + Returns: + List of resource instances validated against MODEL if defined. + """ + client = get_client() + + # Use provided tenant_guid or fall back to client.tenant_guid + tenant_guid = tenant_guid or client.tenant_guid + + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError("Tenant GUID is required for this resource.") + + # Build URL: v1.0/tenants/{tenant}/{resource_name}/all + # Manually construct URL to avoid graph_guid being inserted when REQUIRE_GRAPH_GUID is True + url = f"v1.0/tenants/{tenant_guid}/{cls.RESOURCE_NAME}/all" + + instance = client.request("GET", url) + + return ( + [cls.MODEL.model_validate(item) for item in instance] + if getattr(cls, "MODEL", None) + else instance + ) + + @classmethod + def retrieve_all_graph( + cls, tenant_guid: str | None = None, graph_guid: str | None = None + ) -> list["BaseModel"]: + """ + Retrieve all resources for a graph using the /all endpoint. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/{resource_name}/all + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + + Returns: + List of resource instances validated against MODEL if defined. + """ + client = get_client() + + # Use provided values or fall back to client values + tenant_guid = tenant_guid or client.tenant_guid + graph_guid = graph_guid or client.graph_guid + + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError(TENANT_REQUIRED_ERROR) + if cls.REQUIRE_GRAPH_GUID and not graph_guid: + raise ValueError(GRAPH_REQUIRED_ERROR) + + # Build URL based on REQUIRE_GRAPH_GUID setting + if cls.REQUIRE_GRAPH_GUID: + # Use _get_url_v1 when REQUIRE_GRAPH_GUID is True + url = _get_url_v1(cls, tenant_guid, graph_guid, "all") + else: + # Manually construct URL when REQUIRE_GRAPH_GUID is False + # (can't use _get_url_v1 as it would place graph after resource name) + url = f"v1.0/tenants/{tenant_guid}/graphs/{graph_guid}/{cls.RESOURCE_NAME}/all" + + instance = client.request("GET", url) + + return ( + [cls.MODEL.model_validate(item) for item in instance] + if getattr(cls, "MODEL", None) + else instance + ) + + @classmethod + def retrieve_for_graph( + cls, + tenant_guid: str | None = None, + graph_guid: str | None = None, + include_data: bool = False, + include_subordinates: bool = False, + ) -> list["BaseModel"]: + """ + Retrieve resources for a specific graph. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/{resource_name} + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + include_data: Whether to include data in the response. + include_subordinates: Whether to include subordinates in the response. + + Returns: + List of resource instances validated against MODEL if defined. + """ + client = get_client() + + # Use provided values or fall back to client values + tenant_guid = tenant_guid or client.tenant_guid + graph_guid = graph_guid or client.graph_guid + + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError(TENANT_REQUIRED_ERROR) + if not graph_guid: + raise ValueError(GRAPH_REQUIRED_ERROR) + + # Build URL: v1.0/tenants/{tenant}/graphs/{graph}/{resource_name} + # Use a temporary class with REQUIRE_GRAPH_GUID=True to get correct URL structure + class _TempGraphClass: + RESOURCE_NAME = cls.RESOURCE_NAME + REQUIRE_TENANT = cls.REQUIRE_TENANT + REQUIRE_GRAPH_GUID = True + + include = {} + if include_data: + include["incldata"] = None + if include_subordinates: + include["inclsub"] = None + + url = _get_url_v1(_TempGraphClass, tenant_guid, graph_guid, **include) + + instance = client.request("GET", url) + + return ( + [cls.MODEL.model_validate(item) for item in instance] + if getattr(cls, "MODEL", None) + else instance + ) + + +class DeletableAllEndpointMixin: + """ + Mixin class for deleting all resources using the /all endpoint. + Provides methods for both tenant-level and graph-level deletion. + """ + + RESOURCE_NAME: str = "" + REQUIRE_TENANT: bool = True + REQUIRE_GRAPH_GUID: bool = True + + @classmethod + def delete_all_tenant(cls, tenant_guid: str | None = None) -> None: + """ + Delete all resources for a tenant using the /all endpoint. + + Endpoint: + /v1.0/tenants/{tenant}/{resource_name}/all + + Args: + tenant_guid: The tenant GUID. + """ + client = get_client() + + # Build URL: v1.0/tenants/{tenant}/{resource_name}/all + # Manually construct URL to avoid graph_guid being inserted when REQUIRE_GRAPH_GUID is True + tenant_guid = tenant_guid or client.tenant_guid + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError("Tenant GUID is required for this resource.") + url = f"v1.0/tenants/{tenant_guid}/{cls.RESOURCE_NAME}/all" + + client.request("DELETE", url, headers=JSON_CONTENT_TYPE) + + @classmethod + def delete_all_graph( + cls, tenant_guid: str | None = None, graph_guid: str | None = None + ) -> None: + """ + Delete all resources for a graph using the /all endpoint. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/{resource_name}/all + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + """ + client = get_client() + + # Use provided values or fall back to client values + tenant_guid = tenant_guid or client.tenant_guid + graph_guid = graph_guid or client.graph_guid + + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError(TENANT_REQUIRED_ERROR) + if cls.REQUIRE_GRAPH_GUID and not graph_guid: + raise ValueError(GRAPH_REQUIRED_ERROR) + + # Build URL based on REQUIRE_GRAPH_GUID setting + if cls.REQUIRE_GRAPH_GUID: + # Use _get_url_v1 when REQUIRE_GRAPH_GUID is True + url = _get_url_v1(cls, tenant_guid, graph_guid, "all") + else: + # Manually construct URL when REQUIRE_GRAPH_GUID is False + # (can't use _get_url_v1 as it would place graph after resource name) + url = f"v1.0/tenants/{tenant_guid}/graphs/{graph_guid}/{cls.RESOURCE_NAME}/all" + + client.request("DELETE", url, headers=JSON_CONTENT_TYPE) + + @classmethod + def delete_for_graph( + cls, + tenant_guid: str | None = None, + graph_guid: str | None = None, + ) -> None: + """ + Delete resources for a specific graph. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/{resource_name} + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + """ + client = get_client() + + # Use provided values or fall back to client values + tenant_guid = tenant_guid or client.tenant_guid + graph_guid = graph_guid or client.graph_guid + + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError(TENANT_REQUIRED_ERROR) + if not graph_guid: + raise ValueError(GRAPH_REQUIRED_ERROR) + + # Build URL: v1.0/tenants/{tenant}/graphs/{graph}/{resource_name} + # Use a temporary class with REQUIRE_GRAPH_GUID=True to get correct URL structure + class _TempGraphClass: + RESOURCE_NAME = cls.RESOURCE_NAME + REQUIRE_TENANT = cls.REQUIRE_TENANT + REQUIRE_GRAPH_GUID = True + + url = _get_url_v1(_TempGraphClass, tenant_guid, graph_guid) + + client.request("DELETE", url, headers=JSON_CONTENT_TYPE) + + +class RetrievableNodeResourceMixin: + """ + Mixin class for retrieving resources associated with a specific node. + Provides method for retrieving node-specific resources. + + Endpoint pattern: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/{resource_name} + """ + + RESOURCE_NAME: str = "" + MODEL: Optional[Type[BaseModel]] = None + REQUIRE_TENANT: bool = True + REQUIRE_GRAPH_GUID: bool = True + + @classmethod + def retrieve_for_node( + cls, + node_guid: str, + tenant_guid: str | None = None, + graph_guid: str | None = None, + ) -> list["BaseModel"]: + """ + Retrieve resources for a specific node. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/{resource_name} + + Args: + node_guid: The node GUID. + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + + Returns: + List of resource instances validated against MODEL if defined. + """ + client = get_client() + + # Use provided values or fall back to client values + tenant_guid = tenant_guid or client.tenant_guid + graph_guid = graph_guid or client.graph_guid + + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError(TENANT_REQUIRED_ERROR) + if cls.REQUIRE_GRAPH_GUID and not graph_guid: + raise ValueError(GRAPH_REQUIRED_ERROR) + + # Build URL: v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/{resource_name} + # Use _get_url_base with a temporary class that has RESOURCE_NAME = "nodes" + # to build the tenant/graph/nodes part, then append the actual resource name + # Note: REQUIRE_GRAPH_GUID must be True to include graphs/{graph} in the path + class _TempNodeClass: + RESOURCE_NAME = "nodes" + REQUIRE_TENANT = cls.REQUIRE_TENANT + REQUIRE_GRAPH_GUID = ( + True # Always True for node endpoints (they require graph) + ) + + # Build base path: tenants/{tenant}/graphs/{graph}/nodes/{node} + base_path = _get_url_base(_TempNodeClass, tenant_guid, graph_guid, node_guid) + # Append the actual resource name + url = f"v1.0/{base_path}/{cls.RESOURCE_NAME}" + + instance = client.request("GET", url) + + return ( + [cls.MODEL.model_validate(item) for item in instance] + if getattr(cls, "MODEL", None) + else instance + ) + + +class RetrievableEdgeResourceMixin: + """ + Mixin class for retrieving resources associated with a specific edge. + Provides method for retrieving edge-specific resources. + + Endpoint pattern: + /v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/{resource_name} + """ + + RESOURCE_NAME: str = "" + MODEL: Optional[Type[BaseModel]] = None + REQUIRE_TENANT: bool = True + REQUIRE_GRAPH_GUID: bool = True + + @classmethod + def retrieve_for_edge( + cls, + edge_guid: str, + tenant_guid: str | None = None, + graph_guid: str | None = None, + ) -> list["BaseModel"]: + """ + Retrieve resources for a specific edge. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/{resource_name} + + Args: + edge_guid: The edge GUID. + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + + Returns: + List of resource instances validated against MODEL if defined. + """ + client = get_client() + + # Use provided values or fall back to client values + tenant_guid = tenant_guid or client.tenant_guid + graph_guid = graph_guid or client.graph_guid + + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError(TENANT_REQUIRED_ERROR) + if cls.REQUIRE_GRAPH_GUID and not graph_guid: + raise ValueError(GRAPH_REQUIRED_ERROR) + + # Build URL: v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/{resource_name} + # Use _get_url_base with a temporary class that has RESOURCE_NAME = "edges" + # to build the tenant/graph/edges part, then append the actual resource name + # Note: REQUIRE_GRAPH_GUID must be True to include graphs/{graph} in the path + class _TempEdgeClass: + RESOURCE_NAME = "edges" + REQUIRE_TENANT = cls.REQUIRE_TENANT + REQUIRE_GRAPH_GUID = ( + True # Always True for edge endpoints (they require graph) + ) + + # Build base path: tenants/{tenant}/graphs/{graph}/edges/{edge} + base_path = _get_url_base(_TempEdgeClass, tenant_guid, graph_guid, edge_guid) + # Append the actual resource name + url = f"v1.0/{base_path}/{cls.RESOURCE_NAME}" + + instance = client.request("GET", url) + + return ( + [cls.MODEL.model_validate(item) for item in instance] + if getattr(cls, "MODEL", None) + else instance + ) + + +class DeletableGraphResourceMixin: + """ + Mixin class for deleting resources at the graph level. + Provides method for deleting graph-specific resources. + + Endpoint pattern: + /v1.0/tenants/{tenant}/graphs/{graph}/{resource_name} + """ + + RESOURCE_NAME: str = "" + REQUIRE_TENANT: bool = True + REQUIRE_GRAPH_GUID: bool = True + + @classmethod + def delete_for_graph( + cls, tenant_guid: str | None = None, graph_guid: str | None = None + ) -> None: + """ + Delete resources for a specific graph. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/{resource_name} + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + """ + client = get_client() + + # Use provided values or fall back to client values + tenant_guid = tenant_guid or client.tenant_guid + graph_guid = graph_guid or client.graph_guid + + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError(TENANT_REQUIRED_ERROR) + if cls.REQUIRE_GRAPH_GUID and not graph_guid: + raise ValueError(GRAPH_REQUIRED_ERROR) + + # Build URL: v1.0/tenants/{tenant}/graphs/{graph}/{resource_name} + # Manually construct URL to ensure correct path structure + url = f"v1.0/tenants/{tenant_guid}/graphs/{graph_guid}/{cls.RESOURCE_NAME}" + + client.request("DELETE", url, headers=JSON_CONTENT_TYPE) + + +class DeletableNodeResourceMixin: + """ + Mixin class for deleting resources associated with a specific node. + Provides method for deleting node-specific resources. + + Endpoint pattern: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/{resource_name} + """ + + RESOURCE_NAME: str = "" + REQUIRE_TENANT: bool = True + REQUIRE_GRAPH_GUID: bool = True + + @classmethod + def delete_for_node( + cls, + node_guid: str, + tenant_guid: str | None = None, + graph_guid: str | None = None, + ) -> None: + """ + Delete resources for a specific node. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/{resource_name} + + Args: + node_guid: The node GUID. + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + """ + client = get_client() + + # Use provided values or fall back to client values + tenant_guid = tenant_guid or client.tenant_guid + graph_guid = graph_guid or client.graph_guid + + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError(TENANT_REQUIRED_ERROR) + if cls.REQUIRE_GRAPH_GUID and not graph_guid: + raise ValueError(GRAPH_REQUIRED_ERROR) + + # Build URL: v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/{resource_name} + # Use _get_url_base with a temporary class that has RESOURCE_NAME = "nodes" + # to build the tenant/graph/nodes part, then append the actual resource name + # Note: REQUIRE_GRAPH_GUID must be True to include graphs/{graph} in the path + class _TempNodeClass: + RESOURCE_NAME = "nodes" + REQUIRE_TENANT = cls.REQUIRE_TENANT + REQUIRE_GRAPH_GUID = ( + True # Always True for node endpoints (they require graph) + ) + + # Build base path: tenants/{tenant}/graphs/{graph}/nodes/{node} + base_path = _get_url_base(_TempNodeClass, tenant_guid, graph_guid, node_guid) + # Append the actual resource name + url = f"v1.0/{base_path}/{cls.RESOURCE_NAME}" + + client.request("DELETE", url, headers=JSON_CONTENT_TYPE) + + +class DeletableEdgeResourceMixin: + """ + Mixin class for deleting resources associated with a specific edge. + Provides method for deleting edge-specific resources. + + Endpoint pattern: + /v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/{resource_name} + """ + + RESOURCE_NAME: str = "" + REQUIRE_TENANT: bool = True + REQUIRE_GRAPH_GUID: bool = True + + @classmethod + def delete_for_edge( + cls, + edge_guid: str, + tenant_guid: str | None = None, + graph_guid: str | None = None, + ) -> None: + """ + Delete resources for a specific edge. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/{resource_name} + + Args: + edge_guid: The edge GUID. + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + """ + client = get_client() + + # Use provided values or fall back to client values + tenant_guid = tenant_guid or client.tenant_guid + graph_guid = graph_guid or client.graph_guid + + if cls.REQUIRE_TENANT and tenant_guid is None: + raise ValueError(TENANT_REQUIRED_ERROR) + if cls.REQUIRE_GRAPH_GUID and not graph_guid: + raise ValueError(GRAPH_REQUIRED_ERROR) + + # Build URL: v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/{resource_name} + # Use _get_url_base with a temporary class that has RESOURCE_NAME = "edges" + # to build the tenant/graph/edges part, then append the actual resource name + # Note: REQUIRE_GRAPH_GUID must be True to include graphs/{graph} in the path + class _TempEdgeClass: + RESOURCE_NAME = "edges" + REQUIRE_TENANT = cls.REQUIRE_TENANT + REQUIRE_GRAPH_GUID = ( + True # Always True for edge endpoints (they require graph) + ) + + # Build base path: tenants/{tenant}/graphs/{graph}/edges/{edge} + base_path = _get_url_base(_TempEdgeClass, tenant_guid, graph_guid, edge_guid) + # Append the actual resource name + url = f"v1.0/{base_path}/{cls.RESOURCE_NAME}" + + client.request("DELETE", url, headers=JSON_CONTENT_TYPE) diff --git a/src/litegraph/models/node.py b/src/litegraph/models/node.py index 87396e3..d8b3966 100755 --- a/src/litegraph/models/node.py +++ b/src/litegraph/models/node.py @@ -23,6 +23,9 @@ class NodeModel(BaseModel): ) name: Optional[str] = Field(default=None, alias="Name") data: Optional[dict] = Field(default=None, alias="Data") # Object + edges_in: Optional[int] = Field(default=None, alias="EdgesIn") + edges_out: Optional[int] = Field(default=None, alias="EdgesOut") + edges_total: Optional[int] = Field(default=None, alias="EdgesTotal") created_utc: datetime = Field( default_factory=lambda: datetime.now(timezone.utc), alias="CreatedUtc" ) diff --git a/src/litegraph/resources/credentials.py b/src/litegraph/resources/credentials.py index 933026c..6a45d7d 100644 --- a/src/litegraph/resources/credentials.py +++ b/src/litegraph/resources/credentials.py @@ -1,3 +1,6 @@ +from typing import Any + +from ..configuration import get_client from ..mixins import ( AllRetrievableAPIResource, CreateableAPIResource, @@ -11,6 +14,7 @@ ) from ..models.credential import CredentialModel from ..models.enumeration_result import EnumerationResultModel +from ..utils.url_helper import _get_url_v1 class Credential( @@ -36,3 +40,71 @@ def enumerate_with_query(cls, **kwargs) -> EnumerationResultModel: Enumerate credentials with a query. """ return super().enumerate_with_query(_data=kwargs) + + @classmethod + def get_bearer_credentials(cls, bearer_token: str) -> Any: + """ + Get credential details for a bearer token. + + Calls: + /v1.0/credentials/bearer/{bearerToken} + + Args: + bearer_token: The bearer token to look up. + + Returns: + Parsed response using MODEL if cls.MODEL is defined, + otherwise the raw response from the client. + """ + client = get_client() + + # Build URL manually: v1.0/credentials/bearer/{bearer_token} + # This endpoint doesn't follow the tenant/graph/resource pattern + url = f"v1.0/{cls.RESOURCE_NAME}/bearer/{bearer_token}" + + instance = client.request("GET", url) + + return ( + cls.MODEL.model_validate(instance) + if getattr(cls, "MODEL", None) + else instance + ) + + @classmethod + def delete_all_tenant_credentials(cls, tenant_guid: str) -> None: + """ + Delete credentials for the given tenant. + + Calls: + /v1.0/tenants/{tenant_guid}/credentials + + Args: + tenant_guid: The tenant GUID whose credentials should be deleted. + """ + client = get_client() + + # Build URL: v1.0/tenants/{tenant}/credentials + url = _get_url_v1(cls, tenant_guid) + + # Perform DELETE request + client.request("DELETE", url) + + @classmethod + def delete_user_credentials(cls, tenant_guid: str, user_guid: str) -> None: + """ + Delete credentials for a specific user under a tenant. + + Calls: + /v1.0/tenants/{tenant_guid}/users/{user_guid}/credentials + + Args: + tenant_guid: Tenant GUID. + user_guid: User GUID whose credentials will be deleted. + """ + client = get_client() + + # Build: + # v1.0/tenants/{tenant}/users/{user_guid}/credentials + url = _get_url_v1(cls, tenant_guid, user_guid, "credentials") + + client.request("DELETE", url) diff --git a/src/litegraph/resources/edges.py b/src/litegraph/resources/edges.py index d07933a..e1d3579 100755 --- a/src/litegraph/resources/edges.py +++ b/src/litegraph/resources/edges.py @@ -1,3 +1,4 @@ +from ..configuration import get_client from ..mixins import ( AllRetrievableAPIResource, CreateableAPIResource, @@ -8,6 +9,7 @@ EnumerableAPIResource, EnumerableAPIResourceWithData, ExistsAPIResource, + RetrievableAllEndpointMixin, RetrievableAPIResource, RetrievableFirstMixin, RetrievableManyMixin, @@ -32,6 +34,7 @@ class Edge( DeleteAllAPIResource, EnumerableAPIResource, EnumerableAPIResourceWithData, + RetrievableAllEndpointMixin, RetrievableFirstMixin, RetrievableManyMixin, ): @@ -59,3 +62,107 @@ def retrieve_first( """ graph_id = graph_id or kwargs.get("graph_guid") return super().retrieve_first(graph_id=graph_id, **kwargs) + + @classmethod + def retrieve_all(cls, **kwargs) -> list[EdgeModel]: + """ + Retrieve all edges. + """ + return super().retrieve_all(**kwargs) + + @classmethod + def retrieve_all_graph_edges( + cls, tenant_guid: str, graph_guid: str + ) -> list[EdgeModel]: + """ + Get all edges for a graph inside a tenant. + + Calls: + /v1.0/tenants/{tenant_guid}/graphs/{graph_guid}/edges/all + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + + Returns: + List of EdgeModel instances or raw response if MODEL is not defined. + """ + return super().retrieve_all_graph(tenant_guid, graph_guid) + + @classmethod + def retrieve_all_tenant_edges( + cls, tenant_guid: str | None = None + ) -> list[EdgeModel]: + """ + Retrieve all edges for a tenant (no graph required). + Endpoint: + /v1.0/tenants/{tenant}/edges/all + """ + return super().retrieve_all_tenant(tenant_guid) + + @classmethod + def delete_all_tenant_edges(cls, tenant_guid: str): + """ + Retrieve all edges for a tenant (no graph required). + Endpoint: + /v1.0/tenants/{tenant}/edges/all + """ + client = get_client() + + # Construct URL manually because this endpoint does NOT use graph_guid + url = f"v1.0/tenants/{tenant_guid}/edges/all" + + instance = client.request("DELETE", url) + + return instance + + @classmethod + def delete_node_edges(cls, tenant_guid: str, graph_guid: str, node_guid: str): + """ + Delete all edges for a specific node inside a graph. + + Calls: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node_guid}/edges + + Args: + tenant_guid: Tenant GUID. + graph_guid: Graph GUID. + node_guid: Node GUID whose edges will be deleted. + + Returns: + Raw API response. + """ + client = get_client() + + # Construct URL manually (because this is node → edges, not edge → nodes) + url = f"v1.0/tenants/{tenant_guid}/graphs/{graph_guid}/nodes/{node_guid}/edges" + + instance = client.request("DELETE", url) + + return instance + + @classmethod + def delete_node_edges_bulk( + cls, tenant_guid: str, graph_guid: str, node_guids: list[str] + ): + """ + Bulk delete edges for multiple nodes inside a graph. + + Calls: + DELETE /v1.0/tenants/{tenant}/graphs/{graph}/nodes/edges/bulk + + Args: + tenant_guid: Tenant GUID. + graph_guid: Graph GUID. + node_guids: List of node GUIDs whose edges will be deleted. + + Returns: + Raw response from the API. + """ + client = get_client() + + # Construct URL manually + url = f"v1.0/tenants/{tenant_guid}/graphs/{graph_guid}/nodes/edges/bulk" + + instance = client.request("DELETE", url, json=node_guids) + return instance diff --git a/src/litegraph/resources/graphs.py b/src/litegraph/resources/graphs.py index ea1c4de..64e58f6 100755 --- a/src/litegraph/resources/graphs.py +++ b/src/litegraph/resources/graphs.py @@ -6,11 +6,13 @@ from ..mixins import ( AllRetrievableAPIResource, CreateableAPIResource, + DeletableAllEndpointMixin, DeletableAPIResource, EnumerableAPIResource, EnumerableAPIResourceWithData, ExistsAPIResource, ExportGexfMixin, + RetrievableAllEndpointMixin, RetrievableAPIResource, RetrievableFirstMixin, RetrievableManyMixin, @@ -38,6 +40,8 @@ class Graph( EnumerableAPIResource, EnumerableAPIResourceWithData, RetrievableStatisticsMixin, + RetrievableAllEndpointMixin, + DeletableAllEndpointMixin, RetrievableFirstMixin, RetrievableManyMixin, ): @@ -224,3 +228,25 @@ def retrieve_subgraph( ) response = client.request("GET", url) return GraphModel.model_validate(response) + + @classmethod + def retrieve_all_tenant_graphs( + cls, tenant_guid: str | None = None + ) -> list[GraphModel]: + """ + Retrieve all graphs for a tenant. + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + + Returns: + List of GraphModel instances. + """ + return super().retrieve_all_tenant(tenant_guid) + + @classmethod + def delete_all_tenant_graphs(cls, tenant_guid: str | None = None) -> None: + """ + Delete all graphs for a tenant. + """ + return super().delete_all_tenant(tenant_guid) diff --git a/src/litegraph/resources/labels.py b/src/litegraph/resources/labels.py index 7162f27..2db8f43 100644 --- a/src/litegraph/resources/labels.py +++ b/src/litegraph/resources/labels.py @@ -2,12 +2,19 @@ AllRetrievableAPIResource, CreateableAPIResource, CreateableMultipleAPIResource, + DeletableAllEndpointMixin, DeletableAPIResource, + DeletableEdgeResourceMixin, + DeletableGraphResourceMixin, + DeletableNodeResourceMixin, EnumerableAPIResource, EnumerableAPIResourceWithData, ExistsAPIResource, + RetrievableAllEndpointMixin, RetrievableAPIResource, + RetrievableEdgeResourceMixin, RetrievableManyMixin, + RetrievableNodeResourceMixin, UpdatableAPIResource, ) from ..models.enumeration_result import EnumerationResultModel @@ -22,9 +29,16 @@ class Label( CreateableMultipleAPIResource, UpdatableAPIResource, DeletableAPIResource, + DeletableAllEndpointMixin, + DeletableEdgeResourceMixin, + DeletableGraphResourceMixin, + DeletableNodeResourceMixin, EnumerableAPIResource, EnumerableAPIResourceWithData, + RetrievableAllEndpointMixin, + RetrievableEdgeResourceMixin, RetrievableManyMixin, + RetrievableNodeResourceMixin, ): """Labels resource.""" @@ -38,3 +52,153 @@ def enumerate_with_query(cls, **kwargs) -> EnumerationResultModel: Enumerate labels with a query. """ return super().enumerate_with_query(_data=kwargs) + + @classmethod + def retrieve_all_tenant_labels( + cls, tenant_guid: str | None = None + ) -> list[LabelModel]: + """ + Retrieve all labels for a tenant. + Endpoint: + /v1.0/tenants/{tenant}/labels/all + """ + return super().retrieve_all_tenant(tenant_guid) + + @classmethod + def retrieve_all_graph_labels( + cls, tenant_guid: str, graph_guid: str + ) -> list[LabelModel]: + """ + Retrieve all labels for a graph. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/labels/all + """ + return super().retrieve_all_graph(tenant_guid, graph_guid) + + @classmethod + def retrieve_graph_labels( + cls, + tenant_guid: str | None = None, + graph_guid: str | None = None, + include_data: bool = False, + include_subordinates: bool = False, + ) -> list[LabelModel]: + """ + Retrieve labels for a specific graph. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/labels + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + include_data: Whether to include data in the response. + include_subordinates: Whether to include subordinates in the response. + + Returns: + List of LabelModel instances. + """ + return super().retrieve_for_graph( + tenant_guid, graph_guid, include_data, include_subordinates + ) + + @classmethod + def retrieve_node_labels( + cls, tenant_guid: str, graph_guid: str, node_guid: str + ) -> list[LabelModel]: + """ + Retrieve labels for a specific node. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/labels + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + node_guid: The node GUID. + + Returns: + List of LabelModel instances. + """ + return super().retrieve_for_node(node_guid, tenant_guid, graph_guid) + + @classmethod + def retrieve_edge_labels( + cls, tenant_guid: str, graph_guid: str, edge_guid: str + ) -> list[LabelModel]: + """ + Retrieve labels for a specific edge. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/labels + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + edge_guid: The edge GUID. + + Returns: + List of LabelModel instances. + """ + return super().retrieve_for_edge(edge_guid, tenant_guid, graph_guid) + + @classmethod + def delete_all_tenant_labels(cls, tenant_guid: str) -> None: + """ + Delete all labels for a tenant. + Endpoint: + /v1.0/tenants/{tenant}/labels/all + """ + return super().delete_all_tenant(tenant_guid) + + @classmethod + def delete_all_graph_labels(cls, tenant_guid: str, graph_guid: str) -> None: + """ + Delete all labels for a graph. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/labels/all + """ + return super().delete_all_graph(tenant_guid, graph_guid) + + @classmethod + def delete_graph_labels(cls, tenant_guid: str, graph_guid: str) -> None: + """ + Delete labels for a specific graph. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/labels + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + """ + return super().delete_for_graph(tenant_guid, graph_guid) + + @classmethod + def delete_node_labels( + cls, tenant_guid: str, graph_guid: str, node_guid: str + ) -> None: + """ + Delete labels for a specific node. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/labels + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + node_guid: The node GUID. + """ + return super().delete_for_node(node_guid, tenant_guid, graph_guid) + + @classmethod + def delete_edge_labels( + cls, tenant_guid: str, graph_guid: str, edge_guid: str + ) -> None: + """ + Delete labels for a specific edge. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/labels + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + edge_guid: The edge GUID. + """ + return super().delete_for_edge(edge_guid, tenant_guid, graph_guid) diff --git a/src/litegraph/resources/nodes.py b/src/litegraph/resources/nodes.py index 3260147..84df15b 100755 --- a/src/litegraph/resources/nodes.py +++ b/src/litegraph/resources/nodes.py @@ -1,13 +1,16 @@ +from ..configuration import get_client from ..mixins import ( AllRetrievableAPIResource, CreateableAPIResource, CreateableMultipleAPIResource, + DeletableAllEndpointMixin, DeletableAPIResource, DeleteAllAPIResource, DeleteMultipleAPIResource, EnumerableAPIResource, EnumerableAPIResourceWithData, ExistsAPIResource, + RetrievableAllEndpointMixin, RetrievableAPIResource, RetrievableFirstMixin, RetrievableManyMixin, @@ -17,6 +20,7 @@ from ..models.enumeration_result import EnumerationResultModel from ..models.node import NodeModel from ..models.search_node_edge import SearchRequest, SearchResult +from ..utils.url_helper import _get_url_v1 class Node( @@ -34,6 +38,8 @@ class Node( EnumerableAPIResourceWithData, RetrievableFirstMixin, RetrievableManyMixin, + RetrievableAllEndpointMixin, + DeletableAllEndpointMixin, ): """ Node resource class. @@ -59,3 +65,94 @@ def retrieve_first( """ graph_id = graph_id or kwargs.get("graph_guid") return super().retrieve_first(graph_id=graph_id, **kwargs) + + @classmethod + def delete_all_tenant_nodes(cls, tenant_guid: str) -> None: + """ + Delete all nodes for a tenant. + + Endpoint: + /v1.0/tenants/{tenant}/nodes/all + + Args: + tenant_guid: The tenant GUID. + """ + return super().delete_all_tenant(tenant_guid) + + @classmethod + def retrieve_all_tenant_nodes( + cls, tenant_guid: str | None = None + ) -> list[NodeModel]: + """ + Retrieve all nodes for a tenant. + """ + return super().retrieve_all_tenant(tenant_guid) + + @classmethod + def retrieve_all_graph_nodes( + cls, tenant_guid: str, graph_guid: str + ) -> list[NodeModel]: + """ + Retrieve all nodes for a graph. + """ + return super().retrieve_all_graph(tenant_guid, graph_guid) + + @classmethod + def retrieve_most_connected_nodes( + cls, tenant_guid: str, graph_guid: str + ) -> list[NodeModel]: + """ + Retrieve the most connected nodes in a graph. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/mostconnected + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + + Returns: + List of NodeModel instances with connection statistics (EdgesIn, EdgesOut, EdgesTotal). + """ + client = get_client() + + # Build URL: v1.0/tenants/{tenant}/graphs/{graph}/nodes/mostconnected + url = _get_url_v1(cls, tenant_guid, graph_guid, "mostconnected") + + instance = client.request("GET", url) + + return ( + [cls.MODEL.model_validate(item) for item in instance] + if getattr(cls, "MODEL", None) + else instance + ) + + @classmethod + def retrieve_least_connected_nodes( + cls, tenant_guid: str, graph_guid: str + ) -> list[NodeModel]: + """ + Retrieve the least connected nodes in a graph. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/leastconnected + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + + Returns: + List of NodeModel instances with connection statistics (EdgesIn, EdgesOut, EdgesTotal). + """ + client = get_client() + + # Build URL: v1.0/tenants/{tenant}/graphs/{graph}/nodes/leastconnected + url = _get_url_v1(cls, tenant_guid, graph_guid, "leastconnected") + + instance = client.request("GET", url) + + return ( + [cls.MODEL.model_validate(item) for item in instance] + if getattr(cls, "MODEL", None) + else instance + ) diff --git a/src/litegraph/resources/tags.py b/src/litegraph/resources/tags.py index 457fdd7..5381a8d 100644 --- a/src/litegraph/resources/tags.py +++ b/src/litegraph/resources/tags.py @@ -2,13 +2,20 @@ AllRetrievableAPIResource, CreateableAPIResource, CreateableMultipleAPIResource, + DeletableAllEndpointMixin, DeletableAPIResource, + DeletableEdgeResourceMixin, + DeletableGraphResourceMixin, + DeletableNodeResourceMixin, DeleteMultipleAPIResource, EnumerableAPIResource, EnumerableAPIResourceWithData, ExistsAPIResource, + RetrievableAllEndpointMixin, RetrievableAPIResource, + RetrievableEdgeResourceMixin, RetrievableManyMixin, + RetrievableNodeResourceMixin, UpdatableAPIResource, ) from ..models.enumeration_result import EnumerationResultModel @@ -23,10 +30,17 @@ class Tag( CreateableMultipleAPIResource, UpdatableAPIResource, DeletableAPIResource, + DeletableAllEndpointMixin, + DeletableGraphResourceMixin, + DeletableNodeResourceMixin, + DeletableEdgeResourceMixin, DeleteMultipleAPIResource, EnumerableAPIResource, EnumerableAPIResourceWithData, + RetrievableAllEndpointMixin, + RetrievableEdgeResourceMixin, RetrievableManyMixin, + RetrievableNodeResourceMixin, ): """Tags resource.""" @@ -40,3 +54,114 @@ def enumerate_with_query(cls, **kwargs) -> EnumerationResultModel: Enumerate tags with a query. """ return super().enumerate_with_query(_data=kwargs) + + @classmethod + def retrieve_all_tenant_tags(cls, tenant_guid: str | None = None) -> list[TagModel]: + """ + Retrieve all tags for a tenant. + Endpoint: + /v1.0/tenants/{tenant}/tags/all + """ + return super().retrieve_all_tenant(tenant_guid) + + @classmethod + def retrieve_all_graph_tags( + cls, tenant_guid: str, graph_guid: str + ) -> list[TagModel]: + """ + Retrieve all tags for a graph. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/tags/all + """ + return super().retrieve_all_graph(tenant_guid, graph_guid) + + @classmethod + def retrieve_node_tags( + cls, tenant_guid: str, graph_guid: str, node_guid: str + ) -> list[TagModel]: + """ + Retrieve tags for a specific node. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/tags + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + node_guid: The node GUID. + + Returns: + List of TagModel instances. + """ + return super().retrieve_for_node(node_guid, tenant_guid, graph_guid) + + @classmethod + def retrieve_edge_tags( + cls, tenant_guid: str, graph_guid: str, edge_guid: str + ) -> list[TagModel]: + """ + Retrieve tags for a specific edge. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/tags + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + edge_guid: The edge GUID. + + Returns: + List of TagModel instances. + """ + return super().retrieve_for_edge(edge_guid, tenant_guid, graph_guid) + + @classmethod + def delete_all_tenant_tags(cls, tenant_guid: str) -> None: + """ + Delete all tags for a tenant. + Endpoint: + /v1.0/tenants/{tenant}/tags/all + """ + return super().delete_all_tenant(tenant_guid) + + @classmethod + def delete_all_graph_tags(cls, tenant_guid: str, graph_guid: str) -> None: + """ + Delete all tags for a graph. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/tags/all + """ + return super().delete_all_graph(tenant_guid, graph_guid) + + @classmethod + def delete_graph_tags(cls, tenant_guid: str, graph_guid: str) -> None: + """ + Delete tags for a specific graph. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/tags + + Args: + tenant_guid: The tenant GUID. + graph_guid: The graph GUID. + """ + return super().delete_for_graph(tenant_guid, graph_guid) + + @classmethod + def delete_node_tags( + cls, tenant_guid: str, graph_guid: str, node_guid: str + ) -> None: + """ + Delete tags for a specific node. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/tags + """ + return super().delete_for_node(node_guid, tenant_guid, graph_guid) + + @classmethod + def delete_edge_tags( + cls, tenant_guid: str, graph_guid: str, edge_guid: str + ) -> None: + """ + Delete tags for a specific edge. + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/tags + """ + return super().delete_for_edge(edge_guid, tenant_guid, graph_guid) diff --git a/src/litegraph/resources/vectors.py b/src/litegraph/resources/vectors.py index a87fa3f..1346a78 100644 --- a/src/litegraph/resources/vectors.py +++ b/src/litegraph/resources/vectors.py @@ -6,13 +6,19 @@ AllRetrievableAPIResource, CreateableAPIResource, CreateableMultipleAPIResource, + DeletableAllEndpointMixin, DeletableAPIResource, + DeletableEdgeResourceMixin, + DeletableNodeResourceMixin, DeleteMultipleAPIResource, EnumerableAPIResource, EnumerableAPIResourceWithData, ExistsAPIResource, + RetrievableAllEndpointMixin, RetrievableAPIResource, + RetrievableEdgeResourceMixin, RetrievableManyMixin, + RetrievableNodeResourceMixin, UpdatableAPIResource, ) from ..models.enumeration_result import EnumerationResultModel @@ -27,12 +33,18 @@ class Vector( CreateableAPIResource, CreateableMultipleAPIResource, RetrievableAPIResource, + RetrievableAllEndpointMixin, AllRetrievableAPIResource, UpdatableAPIResource, DeletableAPIResource, + DeletableAllEndpointMixin, + DeletableNodeResourceMixin, + DeletableEdgeResourceMixin, EnumerableAPIResource, EnumerableAPIResourceWithData, RetrievableManyMixin, + RetrievableNodeResourceMixin, + RetrievableEdgeResourceMixin, DeleteMultipleAPIResource, ): """Vectors resource.""" @@ -113,3 +125,171 @@ def enumerate_with_query(cls, **kwargs) -> EnumerationResultModel: Enumerate vectors with a query. """ return super().enumerate_with_query(_data=kwargs) + + @classmethod + def delete_all_tenant_vectors(cls, tenant_guid: str | None = None) -> None: + """ + Delete all vectors for a tenant. + """ + return super().delete_all_tenant(tenant_guid) + + @classmethod + def delete_all_graph_vectors( + cls, tenant_guid: str | None = None, graph_guid: str | None = None + ) -> None: + """ + Delete all vectors for a graph. + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + """ + return super().delete_all_graph(tenant_guid, graph_guid) + + @classmethod + def retrieve_all_tenant_vectors( + cls, tenant_guid: str | None = None + ) -> list[VectorMetadataModel]: + """ + Retrieve all vectors for a tenant. + """ + return super().retrieve_all_tenant(tenant_guid) + + @classmethod + def retrieve_all_graph_vectors( + cls, tenant_guid: str | None = None, graph_guid: str | None = None + ) -> list[VectorMetadataModel]: + """ + Retrieve all vectors for a graph. + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + + Returns: + List of VectorMetadataModel instances. + """ + return super().retrieve_all_graph(tenant_guid, graph_guid) + + @classmethod + def retrieve_node_vectors( + cls, + node_guid: str, + tenant_guid: str | None = None, + graph_guid: str | None = None, + ) -> list[VectorMetadataModel]: + """ + Retrieve vectors for a specific node. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/vectors + + Args: + node_guid: The node GUID. + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + + Returns: + List of VectorMetadataModel instances. + """ + return super().retrieve_for_node(node_guid, tenant_guid, graph_guid) + + @classmethod + def retrieve_edge_vectors( + cls, + edge_guid: str, + tenant_guid: str | None = None, + graph_guid: str | None = None, + ) -> list[VectorMetadataModel]: + """ + Retrieve vectors for a specific edge. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/edges/{edge}/vectors + + Args: + edge_guid: The edge GUID. + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + + Returns: + List of VectorMetadataModel instances. + """ + return super().retrieve_for_edge(edge_guid, tenant_guid, graph_guid) + + @classmethod + def retrieve_graph_vectors( + cls, + tenant_guid: str | None = None, + graph_guid: str | None = None, + include_data: bool = False, + include_subordinates: bool = False, + ) -> list[VectorMetadataModel]: + """ + Retrieve vectors for a specific graph. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/vectors + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + include_data: Whether to include data in the response. + include_subordinates: Whether to include subordinates in the response. + + Returns: + List of VectorMetadataModel instances. + """ + return super().retrieve_for_graph( + tenant_guid, graph_guid, include_data, include_subordinates + ) + + @classmethod + def delete_graph_vectors( + cls, + tenant_guid: str | None = None, + graph_guid: str | None = None, + ) -> None: + """ + Delete vectors for a specific graph. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/vectors + + Args: + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + """ + return super().delete_for_graph(tenant_guid, graph_guid) + + @classmethod + def delete_node_vectors( + cls, + node_guid: str, + tenant_guid: str | None = None, + graph_guid: str | None = None, + ) -> None: + """ + Delete vectors for a specific node. + + Endpoint: + /v1.0/tenants/{tenant}/graphs/{graph}/nodes/{node}/vectors + + Args: + node_guid: The node GUID. + tenant_guid: The tenant GUID. If not provided, uses client.tenant_guid. + graph_guid: The graph GUID. If not provided, uses client.graph_guid. + """ + return super().delete_for_node(node_guid, tenant_guid, graph_guid) + + @classmethod + def delete_edge_vectors( + cls, + edge_guid: str, + tenant_guid: str | None = None, + graph_guid: str | None = None, + ) -> None: + """ + Delete vectors for a specific edge. + """ + return super().delete_for_edge(edge_guid, tenant_guid, graph_guid) diff --git a/tests/conftest.py b/tests/conftest.py index e69cf9c..e2a6f99 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,16 @@ """ - Dummy conftest.py for litegraph. +Dummy conftest.py for litegraph. - If you don't know what this is for, just leave it empty. - Read more about conftest.py under: - - https://docs.pytest.org/en/stable/fixture.html - - https://docs.pytest.org/en/stable/writing_plugins.html +If you don't know what this is for, just leave it empty. +Read more about conftest.py under: +- https://docs.pytest.org/en/stable/fixture.html +- https://docs.pytest.org/en/stable/writing_plugins.html """ + +import sys +from pathlib import Path + +# Add the src directory to the Python path so tests can import litegraph +src_path = Path(__file__).parent.parent / "src" +if str(src_path) not in sys.path: + sys.path.insert(0, str(src_path)) diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 5e6c3e9..3c5b3fa 100755 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -22,6 +22,13 @@ RetrievableStatisticsMixin, RetrievableFirstMixin, RetrievableManyMixin, + RetrievableNodeResourceMixin, + RetrievableEdgeResourceMixin, + RetrievableAllEndpointMixin, + DeletableNodeResourceMixin, + DeletableEdgeResourceMixin, + DeletableGraphResourceMixin, + DeletableAllEndpointMixin, ) from pydantic import BaseModel from litegraph.exceptions import SdkException @@ -468,7 +475,7 @@ def test_export_gexf_with_params(mock_client): def test_export_gexf_error_handling(mock_client): """Test error handling during GEXF export.""" # Test with invalid UTF-8 response - mock_client.request.return_value = b'\x80invalid' + mock_client.request.return_value = b"\x80invalid" with pytest.raises(SdkException, match="Error exporting GEXF"): TestExportGexf.export_gexf("test-graph-id") @@ -496,7 +503,7 @@ def test_exists_resource_exception_handling(mock_client): def test_create_resource_exception_handling(mock_client): """Test CreateableAPIResource exception handling.""" test_data = {"id": "test-id", "name": "Test Resource"} - + # Test when client.request raises an exception mock_client.request.side_effect = Exception("API Error") with pytest.raises(Exception, match="API Error"): @@ -511,7 +518,7 @@ def test_create_resource_exception_handling(mock_client): def test_create_multiple_exception_handling(mock_client): """Test CreateableMultipleAPIResource exception handling.""" test_data = [{"id": "test-id-1"}, {"id": "test-id-2"}] - + # Test when client.request raises an exception mock_client.request.side_effect = Exception("Bulk creation failed") with pytest.raises(Exception, match="Bulk creation failed"): @@ -539,7 +546,7 @@ def test_retrieve_resource_exception_handling(mock_client): def test_update_resource_exception_handling(mock_client): """Test UpdatableAPIResource exception handling.""" test_data = {"id": "test-id", "name": "Updated Resource"} - + # Test when client.request raises an exception mock_client.request.side_effect = Exception("Update failed") with pytest.raises(Exception, match="Update failed"): @@ -567,7 +574,7 @@ def test_delete_resource_exception_handling(mock_client): def test_delete_multiple_exception_handling(mock_client): """Test DeleteMultipleAPIResource exception handling.""" resource_ids = ["test-id-1", "test-id-2"] - + # Test when client.request raises an exception mock_client.request.side_effect = Exception("Bulk delete failed") with pytest.raises(Exception, match="Bulk delete failed"): @@ -583,7 +590,7 @@ def test_delete_all_exception_handling(mock_client): """Test DeleteAllAPIResource exception handling.""" valid_uuid = "550e8400-e29b-41d4-a716-446655440000" mock_client.graph_guid = valid_uuid - + # Test when client.request raises an exception mock_client.request.side_effect = Exception("Delete all failed") with pytest.raises(Exception, match="Delete all failed"): @@ -611,7 +618,7 @@ def test_retrieve_all_exception_handling(mock_client): def test_search_exception_handling(mock_client): """Test SearchableAPIResource exception handling.""" search_params = {"query": "test"} - + # Test when client.request raises an exception mock_client.request.side_effect = Exception("Search failed") with pytest.raises(Exception, match="Search failed"): @@ -701,7 +708,9 @@ def test_retrieve_with_include_parameters(mock_client): assert isinstance(result, MockModel) # Test retrieval with both include parameters - result = ResourceModel.retrieve("test-id", include_data=True, include_subordinates=True) + result = ResourceModel.retrieve( + "test-id", include_data=True, include_subordinates=True + ) assert isinstance(result, MockModel) @@ -734,15 +743,24 @@ def test_search_with_include_parameters(mock_client): mock_client.request.side_effect = None # Test search with include_data - result = ResourceModel.search(graph_id="test-graph", query="test", include_data=True) + result = ResourceModel.search( + graph_id="test-graph", query="test", include_data=True + ) assert isinstance(result, MockResponseModel) # Test search with include_subordinates - result = ResourceModel.search(graph_id="test-graph", query="test", include_subordinates=True) + result = ResourceModel.search( + graph_id="test-graph", query="test", include_subordinates=True + ) assert isinstance(result, MockResponseModel) # Test search with both include parameters - result = ResourceModel.search(graph_id="test-graph", query="test", include_data=True, include_subordinates=True) + result = ResourceModel.search( + graph_id="test-graph", + query="test", + include_data=True, + include_subordinates=True, + ) assert isinstance(result, MockResponseModel) @@ -854,54 +872,54 @@ def test_tenant_required_error_coverage(mock_client): # """Test graph GUID required error coverage for mixins that require it.""" # # Set graph_guid to None to trigger GRAPH_REQUIRED_ERROR # mock_client.graph_guid = None - + # # Test CreateableAPIResource # test_data = {"id": "test-id", "name": "Test Resource"} # with pytest.raises(ValueError, match="Graph GUID is required for this resource"): # ResourceModel.create(**test_data) - + # # Test CreateableMultipleAPIResource - this doesn't validate graph_guid the same way # # It only uses it for URL construction, so we need to mock the response # test_data_list = [{"id": "test-id-1"}, {"id": "test-id-2"}] # mock_client.request.return_value = [{"id": "test-id-1"}, {"id": "test-id-2"}] - + # result = ResourceModel.create_multiple(test_data_list) # assert isinstance(result, list) # assert len(result) == 2 # assert all(isinstance(item, MockModel) for item in result) - + # # Test RetrievableAPIResource # with pytest.raises(ValueError, match="Graph GUID is required for this resource"): # ResourceModel.retrieve("test-id") - + # # Test UpdatableAPIResource # with pytest.raises(ValueError, match="Graph GUID is required for this resource"): # ResourceModel.update("test-id", **test_data) - + # # Test DeletableAPIResource # with pytest.raises(ValueError, match="Graph GUID is required for this resource"): # ResourceModel.delete("test-id") - + # # Test DeleteMultipleAPIResource # with pytest.raises(ValueError, match="Graph GUID is required for this resource"): # ResourceModel.delete_multiple(["test-id-1", "test-id-2"]) - + # # Test DeleteAllAPIResource # with pytest.raises(ValueError, match="badly formed hexadecimal UUID string"): # ResourceModel.delete_all() - + # # Test AllRetrievableAPIResource # with pytest.raises(ValueError, match="Graph GUID is required for this resource"): # ResourceModel.retrieve_all() - + # # Test SearchableAPIResource # with pytest.raises(ValueError, match="Graph GUID is required for this resource"): # ResourceModel.search("test-graph-id") - + # # Test RetrievableFirstMixin # with pytest.raises(ValueError, match="Graph GUID is required for this resource"): # ResourceModel.retrieve_first("test-graph-id") - + # # Test RetrievableManyMixin # with pytest.raises(ValueError, match="Graph GUID is required for this resource"): # ResourceModel.retrieve_many(["test-id-1", "test-id-2"], "test-graph-id") @@ -909,8 +927,11 @@ def test_tenant_required_error_coverage(mock_client): def test_tenant_not_required_mixins(mock_client): """Test mixins that don't require tenant GUID.""" + # Create a resource class that doesn't require tenant - class ResourceNoTenant(TestBaseResource, CreateableAPIResource, RetrievableAPIResource): + class ResourceNoTenant( + TestBaseResource, CreateableAPIResource, RetrievableAPIResource + ): REQUIRE_TENANT = False REQUIRE_GRAPH_GUID = False @@ -934,8 +955,11 @@ class ResourceNoTenant(TestBaseResource, CreateableAPIResource, RetrievableAPIRe def test_graph_guid_not_required_mixins(mock_client): """Test mixins that don't require graph GUID.""" + # Create a resource class that doesn't require graph GUID - class ResourceNoGraph(TestBaseResource, CreateableAPIResource, RetrievableAPIResource): + class ResourceNoGraph( + TestBaseResource, CreateableAPIResource, RetrievableAPIResource + ): REQUIRE_GRAPH_GUID = False # Set graph_guid to None @@ -983,72 +1007,74 @@ def test_tenant_guid_validation_edge_cases(mock_client): """Test tenant GUID validation edge cases.""" # Test with None tenant_guid (should raise ValueError) mock_client.tenant_guid = None - + test_data = {"id": "test-id", "name": "Test Resource"} - + # Test CreateableAPIResource with None tenant_guid with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): ResourceModel.create(**test_data) - + # Test with valid tenant_guid mock_client.tenant_guid = "valid-tenant-guid" mock_client.request.return_value = {"id": "test-id", "name": "Test Resource"} - + result = ResourceModel.create(**test_data) assert isinstance(result, MockModel) def test_enumerable_api_resource_with_data_coverage(mock_client): """Test EnumerableAPIResourceWithData coverage for missing blocks.""" + # Test with REQUIRE_TENANT = False class ResourceWithoutTenant(TestBaseResource, EnumerableAPIResourceWithData): REQUIRE_TENANT = False MODEL = MockModel - ENUMERABLE_REQUEST_MODEL = MockModel # Use MockModel instead of EnumerationQueryModel - + ENUMERABLE_REQUEST_MODEL = ( + MockModel # Use MockModel instead of EnumerationQueryModel + ) + mock_client.request.return_value = {"items": [], "total": 0} - + # Pass required field 'id' for MockModel result = ResourceWithoutTenant.enumerate_with_query(id="test-id") assert result - + # Test with MODEL = None class ResourceWithoutModel(TestBaseResource, EnumerableAPIResourceWithData): REQUIRE_TENANT = False MODEL = None ENUMERABLE_REQUEST_MODEL = MockModel - + result = ResourceWithoutModel.enumerate_with_query(id="test-id") assert result - + # Test with include_data and include_subordinates (provide them as True) result = ResourceWithoutTenant.enumerate_with_query( - id="test-id", - include_data=True, - include_subordinates=True + id="test-id", include_data=True, include_subordinates=True ) assert result def test_retrievable_statistics_mixin_coverage(mock_client): """Test RetrievableStatisticsMixin coverage for missing blocks.""" + # Test with REQUIRE_TENANT = False class ResourceWithoutTenant(TestBaseResource, RetrievableStatisticsMixin): REQUIRE_TENANT = False - + mock_client.request.return_value = {"stats": "data"} - + result = ResourceWithoutTenant.retrieve_statistics("test-guid") assert result == {"stats": "data"} - + # Test without resource_guid result = ResourceWithoutTenant.retrieve_statistics() assert result == {"stats": "data"} - + # Test with REQUIRE_TENANT = True but valid tenant class ResourceWithTenant(TestBaseResource, RetrievableStatisticsMixin): REQUIRE_TENANT = True - + mock_client.tenant_guid = "valid-tenant" result = ResourceWithTenant.retrieve_statistics("test-guid") assert result == {"stats": "data"} @@ -1056,19 +1082,20 @@ class ResourceWithTenant(TestBaseResource, RetrievableStatisticsMixin): def test_export_gexf_mixin_coverage(mock_client): """Test ExportGexfMixin coverage for missing blocks.""" + # Test with REQUIRE_TENANT = False class ResourceWithoutTenant(TestBaseResource, ExportGexfMixin): REQUIRE_TENANT = False - + mock_client.request.return_value = b"gexf content" - + result = ResourceWithoutTenant.export_gexf("test-graph-id") assert result == "gexf content" - + # Test with REQUIRE_TENANT = True but valid tenant class ResourceWithTenant(TestBaseResource, ExportGexfMixin): REQUIRE_TENANT = True - + mock_client.tenant_guid = "valid-tenant" result = ResourceWithTenant.export_gexf("test-graph-id") assert result == "gexf content" @@ -1078,7 +1105,7 @@ def test_enumerable_api_resource_coverage(mock_client): class ResourceWithoutTenant(TestBaseResource, EnumerableAPIResource): REQUIRE_TENANT = False MODEL = MockModel - + mock_client.request.return_value = { "Objects": [ { @@ -1101,22 +1128,22 @@ class ResourceWithoutTenant(TestBaseResource, EnumerableAPIResource): "EndOfResults": True, "RecordsRemaining": 0, } - + result = ResourceWithoutTenant.enumerate() result_dict = result.model_dump() assert len(result_dict["objects"]) == 1 assert result_dict["total_records"] == 1 assert result_dict["success"] is True - + class ResourceWithoutModel(TestBaseResource, EnumerableAPIResource): REQUIRE_TENANT = False MODEL = None - + mock_client.request.return_value = {"items": [], "total": 0} - + result = ResourceWithoutModel.enumerate() assert result == {"items": [], "total": 0} - + result = ResourceWithoutTenant.enumerate( include_data=True, include_subordinates=True, @@ -1127,56 +1154,54 @@ class ResourceWithoutModel(TestBaseResource, EnumerableAPIResource): def test_retrievable_first_mixin_coverage(mock_client): """Test RetrievableFirstMixin coverage for missing blocks.""" + # Test with REQUIRE_TENANT = False class ResourceWithoutTenant(TestBaseResource, RetrievableFirstMixin): REQUIRE_TENANT = False REQUIRE_GRAPH_GUID = True MODEL = MockModel SEARCH_MODELS = (MockModel, MockModel) - + mock_client.request.return_value = {"id": "test-id", "name": "Test Resource"} - + # Pass required field 'id' for MockModel result = ResourceWithoutTenant.retrieve_first("test-graph-id", id="test-id") assert isinstance(result, MockModel) assert result.id == "test-id" - + # Test without graph_id (should use else URL path) result = ResourceWithoutTenant.retrieve_first(id="test-id") assert isinstance(result, MockModel) assert result.id == "test-id" - + # Test with MODEL = None class ResourceWithoutModel(TestBaseResource, RetrievableFirstMixin): REQUIRE_TENANT = False REQUIRE_GRAPH_GUID = True MODEL = None SEARCH_MODELS = (MockModel, MockModel) - + result = ResourceWithoutModel.retrieve_first("test-graph-id", id="test-id") assert result == {"id": "test-id", "name": "Test Resource"} - + # Test with include_data and include_subordinates (provide them as True) result = ResourceWithoutTenant.retrieve_first( - "test-graph-id", - id="test-id", - include_data=True, - include_subordinates=True + "test-graph-id", id="test-id", include_data=True, include_subordinates=True ) assert isinstance(result, MockModel) assert result.id == "test-id" - + # Test with REQUIRE_GRAPH_GUID = False class ResourceWithoutGraphGuid(TestBaseResource, RetrievableFirstMixin): REQUIRE_TENANT = False REQUIRE_GRAPH_GUID = False MODEL = MockModel SEARCH_MODELS = (MockModel, MockModel) - + result = ResourceWithoutGraphGuid.retrieve_first("test-graph-id", id="test-id") assert isinstance(result, MockModel) assert result.id == "test-id" - + # Test without graph_id when REQUIRE_GRAPH_GUID = False result = ResourceWithoutGraphGuid.retrieve_first(id="test-id") assert isinstance(result, MockModel) @@ -1185,47 +1210,682 @@ class ResourceWithoutGraphGuid(TestBaseResource, RetrievableFirstMixin): def test_retrievable_many_mixin_coverage(mock_client): """Test RetrievableManyMixin coverage for missing blocks.""" + # Test with REQUIRE_TENANT = False class ResourceWithoutTenant(TestBaseResource, RetrievableManyMixin): REQUIRE_TENANT = False REQUIRE_GRAPH_GUID = True MODEL = MockModel - + mock_client.request.return_value = [ {"id": "test-id-1", "name": "Test Resource 1"}, - {"id": "test-id-2", "name": "Test Resource 2"} + {"id": "test-id-2", "name": "Test Resource 2"}, ] - + # Test with graph_guid (should use first URL path) - result = ResourceWithoutTenant.retrieve_many(["test-id-1", "test-id-2"], "test-graph-guid") + result = ResourceWithoutTenant.retrieve_many( + ["test-id-1", "test-id-2"], "test-graph-guid" + ) assert isinstance(result, list) assert len(result) == 2 assert all(isinstance(item, MockModel) for item in result) - + # Test without graph_guid (should use else URL path) result = ResourceWithoutTenant.retrieve_many(["test-id-1", "test-id-2"]) assert isinstance(result, list) assert len(result) == 2 assert all(isinstance(item, MockModel) for item in result) - + # Test with MODEL = None class ResourceWithoutModel(TestBaseResource, RetrievableManyMixin): REQUIRE_TENANT = False REQUIRE_GRAPH_GUID = True MODEL = None - - result = ResourceWithoutModel.retrieve_many(["test-id-1", "test-id-2"], "test-graph-guid") + + result = ResourceWithoutModel.retrieve_many( + ["test-id-1", "test-id-2"], "test-graph-guid" + ) assert isinstance(result, list) assert len(result) == 2 assert all(isinstance(item, dict) for item in result) - + # Test with REQUIRE_GRAPH_GUID = False class ResourceWithoutGraphGuid(TestBaseResource, RetrievableManyMixin): REQUIRE_TENANT = False REQUIRE_GRAPH_GUID = False MODEL = MockModel - - result = ResourceWithoutGraphGuid.retrieve_many(["test-id-1", "test-id-2"], "test-graph-guid") + + result = ResourceWithoutGraphGuid.retrieve_many( + ["test-id-1", "test-id-2"], "test-graph-guid" + ) + assert isinstance(result, list) + assert len(result) == 2 + assert all(isinstance(item, MockModel) for item in result) + + +def test_retrievable_node_resource_mixin(mock_client): + """Test RetrievableNodeResourceMixin.""" + + class TestNodeResource(TestBaseResource, RetrievableNodeResourceMixin): + RESOURCE_NAME = "tags" + MODEL = MockModel + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test successful retrieval + test_data = [ + {"id": "tag-1", "name": "Tag 1"}, + {"id": "tag-2", "name": "Tag 2"}, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = TestNodeResource.retrieve_for_node("node-guid-123") assert isinstance(result, list) assert len(result) == 2 assert all(isinstance(item, MockModel) for item in result) + assert result[0].id == "tag-1" + assert result[1].id == "tag-2" + + # Verify the request was made correctly + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "GET" + assert "nodes/node-guid-123/tags" in called_args[0][1] + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = TestNodeResource.retrieve_for_node( + "node-guid-123", tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + assert isinstance(result, list) + assert len(result) == 2 + + # Test without tenant_guid when required + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + TestNodeResource.retrieve_for_node("node-guid-123") + + # Test without graph_guid when required + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = None + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestNodeResource.retrieve_for_node("node-guid-123") + + # Test with empty graph_guid + mock_client.graph_guid = "" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestNodeResource.retrieve_for_node("node-guid-123") + + # Test with MODEL = None (should return raw data) + class TestNodeResourceNoModel(TestBaseResource, RetrievableNodeResourceMixin): + RESOURCE_NAME = "tags" + MODEL = None + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + mock_client.graph_guid = "test-graph-guid" + mock_client.request.return_value = test_data + result = TestNodeResourceNoModel.retrieve_for_node("node-guid-123") + assert result == test_data + + # Test with REQUIRE_TENANT = False + class TestNodeResourceNoTenant(TestBaseResource, RetrievableNodeResourceMixin): + RESOURCE_NAME = "tags" + MODEL = MockModel + REQUIRE_TENANT = False + REQUIRE_GRAPH_GUID = True + + mock_client.tenant_guid = None + mock_client.graph_guid = "test-graph-guid" + mock_client.request.return_value = test_data + result = TestNodeResourceNoTenant.retrieve_for_node("node-guid-123") + assert isinstance(result, list) + assert len(result) == 2 + + +def test_retrievable_edge_resource_mixin(mock_client): + """Test RetrievableEdgeResourceMixin.""" + + class TestEdgeResource(TestBaseResource, RetrievableEdgeResourceMixin): + RESOURCE_NAME = "tags" + MODEL = MockModel + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test successful retrieval + test_data = [ + {"id": "tag-1", "name": "Tag 1"}, + {"id": "tag-2", "name": "Tag 2"}, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = TestEdgeResource.retrieve_for_edge("edge-guid-123") + assert isinstance(result, list) + assert len(result) == 2 + assert all(isinstance(item, MockModel) for item in result) + assert result[0].id == "tag-1" + assert result[1].id == "tag-2" + + # Verify the request was made correctly + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "GET" + assert "edges/edge-guid-123/tags" in called_args[0][1] + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = TestEdgeResource.retrieve_for_edge( + "edge-guid-123", tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + assert isinstance(result, list) + assert len(result) == 2 + + # Test without tenant_guid when required + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + TestEdgeResource.retrieve_for_edge("edge-guid-123") + + # Test without graph_guid when required + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = None + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestEdgeResource.retrieve_for_edge("edge-guid-123") + + # Test with empty graph_guid + mock_client.graph_guid = "" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestEdgeResource.retrieve_for_edge("edge-guid-123") + + # Test with MODEL = None (should return raw data) + class TestEdgeResourceNoModel(TestBaseResource, RetrievableEdgeResourceMixin): + RESOURCE_NAME = "tags" + MODEL = None + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + mock_client.graph_guid = "test-graph-guid" + mock_client.request.return_value = test_data + result = TestEdgeResourceNoModel.retrieve_for_edge("edge-guid-123") + assert result == test_data + + # Test with REQUIRE_TENANT = False + class TestEdgeResourceNoTenant(TestBaseResource, RetrievableEdgeResourceMixin): + RESOURCE_NAME = "tags" + MODEL = MockModel + REQUIRE_TENANT = False + REQUIRE_GRAPH_GUID = True + + mock_client.tenant_guid = None + mock_client.graph_guid = "test-graph-guid" + mock_client.request.return_value = test_data + result = TestEdgeResourceNoTenant.retrieve_for_edge("edge-guid-123") + assert isinstance(result, list) + assert len(result) == 2 + + +def test_retrievable_all_endpoint_mixin_retrieve_for_graph(mock_client): + """Test RetrievableAllEndpointMixin.retrieve_for_graph method.""" + + class TestGraphResource(TestBaseResource, RetrievableAllEndpointMixin): + RESOURCE_NAME = "vectors" + MODEL = MockModel + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test successful retrieval + test_data = [ + {"id": "vector-1", "name": "Vector 1"}, + {"id": "vector-2", "name": "Vector 2"}, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = TestGraphResource.retrieve_for_graph() + assert isinstance(result, list) + assert len(result) == 2 + assert all(isinstance(item, MockModel) for item in result) + assert result[0].id == "vector-1" + assert result[1].id == "vector-2" + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = TestGraphResource.retrieve_for_graph( + tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + assert isinstance(result, list) + assert len(result) == 2 + + # Test with include_data + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = TestGraphResource.retrieve_for_graph(include_data=True) + assert isinstance(result, list) + called_args = mock_client.request.call_args + assert "incldata" in called_args[0][1] or "incldata" in str(called_args) + + # Test with include_subordinates + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = TestGraphResource.retrieve_for_graph(include_subordinates=True) + assert isinstance(result, list) + + # Test with both include parameters + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = TestGraphResource.retrieve_for_graph( + include_data=True, include_subordinates=True + ) + assert isinstance(result, list) + + # Test without tenant_guid when required + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + TestGraphResource.retrieve_for_graph() + + # Test without graph_guid when required + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = None + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestGraphResource.retrieve_for_graph() + + # Test with empty graph_guid + mock_client.graph_guid = "" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestGraphResource.retrieve_for_graph() + + # Test with MODEL = None (should return raw data) + class TestGraphResourceNoModel(TestBaseResource, RetrievableAllEndpointMixin): + RESOURCE_NAME = "vectors" + MODEL = None + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + mock_client.graph_guid = "test-graph-guid" + mock_client.request.return_value = test_data + result = TestGraphResourceNoModel.retrieve_for_graph() + assert result == test_data + + # Test with REQUIRE_TENANT = False + class TestGraphResourceNoTenant(TestBaseResource, RetrievableAllEndpointMixin): + RESOURCE_NAME = "vectors" + MODEL = MockModel + REQUIRE_TENANT = False + REQUIRE_GRAPH_GUID = True + + mock_client.tenant_guid = None + mock_client.graph_guid = "test-graph-guid" + mock_client.request.return_value = test_data + result = TestGraphResourceNoTenant.retrieve_for_graph() + assert isinstance(result, list) + assert len(result) == 2 + + +def test_deletable_node_resource_mixin(mock_client): + """Test DeletableNodeResourceMixin.""" + + class TestNodeResource(TestBaseResource, DeletableNodeResourceMixin): + RESOURCE_NAME = "tags" + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test successful deletion + mock_client.request.side_effect = None + TestNodeResource.delete_for_node("node-guid-123") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "nodes/node-guid-123/tags" in called_args[0][1] + assert called_args[1]["headers"] == {"Content-Type": "application/json"} + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + TestNodeResource.delete_for_node( + "node-guid-123", tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "nodes/node-guid-123/tags" in called_args[0][1] + + # Test without tenant_guid when required + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + TestNodeResource.delete_for_node("node-guid-123") + + # Test without graph_guid when required + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = None + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestNodeResource.delete_for_node("node-guid-123") + + # Test with empty graph_guid + mock_client.graph_guid = "" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestNodeResource.delete_for_node("node-guid-123") + + # Test with REQUIRE_TENANT = False + class TestNodeResourceNoTenant(TestBaseResource, DeletableNodeResourceMixin): + RESOURCE_NAME = "tags" + REQUIRE_TENANT = False + REQUIRE_GRAPH_GUID = True + + mock_client.tenant_guid = None + mock_client.graph_guid = "test-graph-guid" + mock_client.request.reset_mock() + TestNodeResourceNoTenant.delete_for_node("node-guid-123") + mock_client.request.assert_called_once() + assert mock_client.request.call_args[0][0] == "DELETE" + + +def test_deletable_edge_resource_mixin(mock_client): + """Test DeletableEdgeResourceMixin.""" + + class TestEdgeResource(TestBaseResource, DeletableEdgeResourceMixin): + RESOURCE_NAME = "tags" + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test successful deletion + mock_client.request.side_effect = None + TestEdgeResource.delete_for_edge("edge-guid-123") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "edges/edge-guid-123/tags" in called_args[0][1] + assert called_args[1]["headers"] == {"Content-Type": "application/json"} + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + TestEdgeResource.delete_for_edge( + "edge-guid-123", tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "edges/edge-guid-123/tags" in called_args[0][1] + + # Test without tenant_guid when required + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + TestEdgeResource.delete_for_edge("edge-guid-123") + + # Test without graph_guid when required + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = None + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestEdgeResource.delete_for_edge("edge-guid-123") + + # Test with empty graph_guid + mock_client.graph_guid = "" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestEdgeResource.delete_for_edge("edge-guid-123") + + # Test with REQUIRE_TENANT = False + class TestEdgeResourceNoTenant(TestBaseResource, DeletableEdgeResourceMixin): + RESOURCE_NAME = "tags" + REQUIRE_TENANT = False + REQUIRE_GRAPH_GUID = True + + mock_client.tenant_guid = None + mock_client.graph_guid = "test-graph-guid" + mock_client.request.reset_mock() + TestEdgeResourceNoTenant.delete_for_edge("edge-guid-123") + mock_client.request.assert_called_once() + assert mock_client.request.call_args[0][0] == "DELETE" + + +def test_deletable_graph_resource_mixin(mock_client): + """Test DeletableGraphResourceMixin.""" + + class TestGraphResource(TestBaseResource, DeletableGraphResourceMixin): + RESOURCE_NAME = "vectors" + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test successful deletion + mock_client.request.side_effect = None + TestGraphResource.delete_for_graph() + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "graphs/test-graph-guid/vectors" in called_args[0][1] + assert called_args[1]["headers"] == {"Content-Type": "application/json"} + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + TestGraphResource.delete_for_graph( + tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "graphs/explicit-graph/vectors" in called_args[0][1] + + # Test without tenant_guid when required + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + TestGraphResource.delete_for_graph() + + # Test without graph_guid when required + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = None + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestGraphResource.delete_for_graph() + + # Test with empty graph_guid + mock_client.graph_guid = "" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestGraphResource.delete_for_graph() + + # Test with REQUIRE_TENANT = False + class TestGraphResourceNoTenant(TestBaseResource, DeletableGraphResourceMixin): + RESOURCE_NAME = "vectors" + REQUIRE_TENANT = False + REQUIRE_GRAPH_GUID = True + + mock_client.tenant_guid = None + mock_client.graph_guid = "test-graph-guid" + mock_client.request.reset_mock() + TestGraphResourceNoTenant.delete_for_graph() + mock_client.request.assert_called_once() + assert mock_client.request.call_args[0][0] == "DELETE" + + +def test_retrievable_node_resource_mixin_exception_handling(mock_client): + """Test RetrievableNodeResourceMixin exception handling.""" + + class TestNodeResource(TestBaseResource, RetrievableNodeResourceMixin): + RESOURCE_NAME = "tags" + MODEL = MockModel + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("Retrieval failed") + with pytest.raises(Exception, match="Retrieval failed"): + TestNodeResource.retrieve_for_node("node-guid-123") + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid node GUID") + with pytest.raises(ValueError, match="Invalid node GUID"): + TestNodeResource.retrieve_for_node("node-guid-123") + + +def test_retrievable_edge_resource_mixin_exception_handling(mock_client): + """Test RetrievableEdgeResourceMixin exception handling.""" + + class TestEdgeResource(TestBaseResource, RetrievableEdgeResourceMixin): + RESOURCE_NAME = "tags" + MODEL = MockModel + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("Retrieval failed") + with pytest.raises(Exception, match="Retrieval failed"): + TestEdgeResource.retrieve_for_edge("edge-guid-123") + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid edge GUID") + with pytest.raises(ValueError, match="Invalid edge GUID"): + TestEdgeResource.retrieve_for_edge("edge-guid-123") + + +def test_retrievable_all_endpoint_mixin_retrieve_for_graph_exception_handling( + mock_client, +): + """Test RetrievableAllEndpointMixin.retrieve_for_graph exception handling.""" + + class TestGraphResource(TestBaseResource, RetrievableAllEndpointMixin): + RESOURCE_NAME = "vectors" + MODEL = MockModel + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("Retrieval failed") + with pytest.raises(Exception, match="Retrieval failed"): + TestGraphResource.retrieve_for_graph() + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid graph GUID") + with pytest.raises(ValueError, match="Invalid graph GUID"): + TestGraphResource.retrieve_for_graph() + + +def test_deletable_node_resource_mixin_exception_handling(mock_client): + """Test DeletableNodeResourceMixin exception handling.""" + + class TestNodeResource(TestBaseResource, DeletableNodeResourceMixin): + RESOURCE_NAME = "tags" + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("Delete failed") + with pytest.raises(Exception, match="Delete failed"): + TestNodeResource.delete_for_node("node-guid-123") + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid node GUID") + with pytest.raises(ValueError, match="Invalid node GUID"): + TestNodeResource.delete_for_node("node-guid-123") + + +def test_deletable_edge_resource_mixin_exception_handling(mock_client): + """Test DeletableEdgeResourceMixin exception handling.""" + + class TestEdgeResource(TestBaseResource, DeletableEdgeResourceMixin): + RESOURCE_NAME = "tags" + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("Delete failed") + with pytest.raises(Exception, match="Delete failed"): + TestEdgeResource.delete_for_edge("edge-guid-123") + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid edge GUID") + with pytest.raises(ValueError, match="Invalid edge GUID"): + TestEdgeResource.delete_for_edge("edge-guid-123") + + +def test_deletable_graph_resource_mixin_exception_handling(mock_client): + """Test DeletableGraphResourceMixin exception handling.""" + + class TestGraphResource(TestBaseResource, DeletableGraphResourceMixin): + RESOURCE_NAME = "vectors" + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("Delete failed") + with pytest.raises(Exception, match="Delete failed"): + TestGraphResource.delete_for_graph() + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid graph GUID") + with pytest.raises(ValueError, match="Invalid graph GUID"): + TestGraphResource.delete_for_graph() + + +def test_deletable_all_endpoint_mixin_delete_for_graph(mock_client): + """Test DeletableAllEndpointMixin.delete_for_graph method.""" + + class TestGraphResource(TestBaseResource, DeletableAllEndpointMixin): + RESOURCE_NAME = "vectors" + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test successful deletion + mock_client.request.side_effect = None + TestGraphResource.delete_for_graph() + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "graphs/test-graph-guid/vectors" in called_args[0][1] + assert called_args[1]["headers"] == {"Content-Type": "application/json"} + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + TestGraphResource.delete_for_graph( + tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "graphs/explicit-graph/vectors" in called_args[0][1] + + # Test without tenant_guid when required + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + TestGraphResource.delete_for_graph() + + # Test without graph_guid when required + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = None + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestGraphResource.delete_for_graph() + + # Test with empty graph_guid + mock_client.graph_guid = "" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + TestGraphResource.delete_for_graph() + + # Test with REQUIRE_TENANT = False + class TestGraphResourceNoTenant(TestBaseResource, DeletableAllEndpointMixin): + RESOURCE_NAME = "vectors" + REQUIRE_TENANT = False + REQUIRE_GRAPH_GUID = True + + mock_client.tenant_guid = None + mock_client.graph_guid = "test-graph-guid" + mock_client.request.reset_mock() + TestGraphResourceNoTenant.delete_for_graph() + mock_client.request.assert_called_once() + assert mock_client.request.call_args[0][0] == "DELETE" + + +def test_deletable_all_endpoint_mixin_delete_for_graph_exception_handling(mock_client): + """Test DeletableAllEndpointMixin.delete_for_graph exception handling.""" + + class TestGraphResource(TestBaseResource, DeletableAllEndpointMixin): + RESOURCE_NAME = "vectors" + REQUIRE_TENANT = True + REQUIRE_GRAPH_GUID = True + + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("Delete failed") + with pytest.raises(Exception, match="Delete failed"): + TestGraphResource.delete_for_graph() + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid graph GUID") + with pytest.raises(ValueError, match="Invalid graph GUID"): + TestGraphResource.delete_for_graph() diff --git a/tests/test_models/test_tags.py b/tests/test_models/test_tags.py new file mode 100644 index 0000000..da6b2a7 --- /dev/null +++ b/tests/test_models/test_tags.py @@ -0,0 +1,254 @@ +from datetime import datetime, timezone +import pytest +from unittest.mock import Mock + +from litegraph.models.tag import TagModel +from litegraph.models.expression import ExprModel +from litegraph.enums.operator_enum import Opertator_Enum +from litegraph.resources.tags import Tag + + +@pytest.fixture +def mock_client(monkeypatch): + """Create a mock client and configure it.""" + client = Mock() + client.base_url = "http://test-api.com" + client.tenant_guid = "test-tenant-guid" + client.graph_guid = "test-graph-guid" + monkeypatch.setattr("litegraph.configuration._client", client) + return client + + +@pytest.fixture +def valid_tag_data(): + """Fixture providing valid tag data.""" + return { + "GUID": "550e8400-e29b-41d4-a716-446655440000", + "TenantGUID": "550e8400-e29b-41d4-a716-446655440001", + "GraphGUID": "550e8400-e29b-41d4-a716-446655440002", + "Key": "test-key", + "Value": "test-value", + "CreatedUtc": "2024-01-01T00:00:00+00:00", + "LastUpdateUtc": "2024-01-01T00:00:00+00:00", + } + + +def test_retrieve_all_tenant_tags(mock_client, valid_tag_data): + """Test retrieve_all_tenant_tags method.""" + test_data = [ + {**valid_tag_data, "GUID": "tag-1", "Key": "key1", "Value": "value1"}, + {**valid_tag_data, "GUID": "tag-2", "Key": "key2", "Value": "value2"}, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = Tag.retrieve_all_tenant_tags() + assert isinstance(result, list) + assert len(result) == 2 + assert all(isinstance(item, TagModel) for item in result) + assert result[0].guid == "tag-1" + assert result[1].guid == "tag-2" + + # Test with explicit tenant_guid + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = Tag.retrieve_all_tenant_tags(tenant_guid="explicit-tenant") + assert isinstance(result, list) + assert len(result) == 2 + + +def test_retrieve_all_graph_tags(mock_client, valid_tag_data): + """Test retrieve_all_graph_tags method.""" + test_data = [ + {**valid_tag_data, "GUID": "tag-1", "Key": "key1", "Value": "value1"}, + {**valid_tag_data, "GUID": "tag-2", "Key": "key2", "Value": "value2"}, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = Tag.retrieve_all_graph_tags("test-tenant", "test-graph") + assert isinstance(result, list) + assert len(result) == 2 + assert all(isinstance(item, TagModel) for item in result) + assert result[0].guid == "tag-1" + assert result[1].guid == "tag-2" + + # Verify the request was made correctly + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "GET" + assert "graphs/test-graph/tags/all" in called_args[0][1] + + +def test_retrieve_node_tags(mock_client, valid_tag_data): + """Test retrieve_node_tags method.""" + test_data = [ + { + **valid_tag_data, + "GUID": "tag-1", + "NodeGUID": "node-guid-123", + "Key": "key1", + "Value": "value1", + }, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = Tag.retrieve_node_tags("test-tenant", "test-graph", "node-guid-123") + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], TagModel) + assert result[0].guid == "tag-1" + assert result[0].node_guid == "node-guid-123" + + # Verify the request was made correctly + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "GET" + assert "nodes/node-guid-123/tags" in called_args[0][1] + + +def test_retrieve_edge_tags(mock_client, valid_tag_data): + """Test retrieve_edge_tags method.""" + test_data = [ + { + **valid_tag_data, + "GUID": "tag-1", + "EdgeGUID": "edge-guid-123", + "Key": "key1", + "Value": "value1", + }, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = Tag.retrieve_edge_tags("test-tenant", "test-graph", "edge-guid-123") + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], TagModel) + assert result[0].guid == "tag-1" + assert result[0].edge_guid == "edge-guid-123" + + # Verify the request was made correctly + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "GET" + assert "edges/edge-guid-123/tags" in called_args[0][1] + + +def test_delete_all_tenant_tags(mock_client): + """Test delete_all_tenant_tags method.""" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + Tag.delete_all_tenant_tags("test-tenant") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "tenants/test-tenant/tags/all" in called_args[0][1] + + +def test_delete_all_graph_tags(mock_client): + """Test delete_all_graph_tags method.""" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + Tag.delete_all_graph_tags("test-tenant", "test-graph") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "graphs/test-graph/tags/all" in called_args[0][1] + + +def test_delete_graph_tags(mock_client): + """Test delete_graph_tags method.""" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + Tag.delete_graph_tags("test-tenant", "test-graph") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "graphs/test-graph/tags" in called_args[0][1] + assert called_args[1]["headers"] == {"Content-Type": "application/json"} + + +def test_delete_node_tags(mock_client): + """Test delete_node_tags method.""" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + Tag.delete_node_tags("test-tenant", "test-graph", "node-guid-123") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "nodes/node-guid-123/tags" in called_args[0][1] + assert called_args[1]["headers"] == {"Content-Type": "application/json"} + + +def test_delete_edge_tags(mock_client): + """Test delete_edge_tags method.""" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + Tag.delete_edge_tags("test-tenant", "test-graph", "edge-guid-123") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "edges/edge-guid-123/tags" in called_args[0][1] + assert called_args[1]["headers"] == {"Content-Type": "application/json"} + + +def test_tag_model_validation(valid_tag_data): + """Test TagModel validation.""" + model = TagModel(**valid_tag_data) + assert isinstance(model.guid, str) + assert isinstance(model.tenant_guid, str) + assert model.graph_guid == valid_tag_data["GraphGUID"] + assert model.key == valid_tag_data["Key"] + assert model.value == valid_tag_data["Value"] + assert model.created_utc == datetime.fromisoformat(valid_tag_data["CreatedUtc"]) + assert model.last_update_utc == datetime.fromisoformat( + valid_tag_data["LastUpdateUtc"] + ) + + +def test_enumerate_with_query(mock_client): + """Test enumerate_with_query method.""" + mock_client.request.return_value = { + "Objects": [ + { + "GUID": "tag-1", + "TenantGUID": "test-tenant", + "Key": "key1", + "Value": "value1", + } + ], + "TotalRecords": 1, + "Success": True, + "Timestamp": { + "Start": datetime.now(timezone.utc).isoformat(), + "End": None, + "Messages": {}, + "Metadata": None, + }, + "MaxResults": 1000, + "IterationsRequired": 0, + "ContinuationToken": None, + "EndOfResults": True, + "RecordsRemaining": 0, + } + mock_client.request.side_effect = None + + # Test with valid EnumerationQueryModel parameters + # Provide a valid expr to avoid ExprModel validation errors + valid_expr = ExprModel(Left="Key", Operator=Opertator_Enum.Equals, Right="test-key") + result = Tag.enumerate_with_query( + labels=["label1"], tags={"key1": "value1"}, expr=valid_expr + ) + assert result is not None + assert hasattr(result, "objects") + assert hasattr(result, "total_records") + assert len(result.objects) == 1 + assert result.total_records == 1 + diff --git a/tests/test_models/test_vector_index.py b/tests/test_models/test_vector_index.py new file mode 100644 index 0000000..fba84d3 --- /dev/null +++ b/tests/test_models/test_vector_index.py @@ -0,0 +1,345 @@ +from datetime import datetime, timezone +import pytest +from unittest.mock import Mock + +from litegraph.enums.vector_index_type_enum import Vector_Index_Type_Enum +from litegraph.models.hnsw_lite_vector_index import HnswLiteVectorIndexModel +from litegraph.models.vector_index_statistics import VectorIndexStatisticsModel +from litegraph.resources.vector_index import VectorIndex + + +@pytest.fixture +def mock_client(monkeypatch): + """Create a mock client and configure it.""" + client = Mock() + client.base_url = "http://test-api.com" + client.tenant_guid = "test-tenant-guid" + client.graph_guid = "test-graph-guid" + monkeypatch.setattr("litegraph.configuration._client", client) + return client + + +@pytest.fixture +def valid_vector_index_config(): + """Fixture providing valid vector index configuration.""" + return { + "GUID": "550e8400-e29b-41d4-a716-446655440000", + "GraphGUID": "550e8400-e29b-41d4-a716-446655440002", + "VectorDimensionality": 128, + "VectorIndexType": Vector_Index_Type_Enum.HnswSqlite, + "M": 16, + "EfConstruction": 200, + "DefaultEf": 50, + "DistanceMetric": "Cosine", + "VectorCount": 0, + "IsLoaded": False, + } + + +@pytest.fixture +def valid_vector_index_stats(): + """Fixture providing valid vector index statistics.""" + return { + "VectorCount": 100, + "Dimensions": 128, + "IndexType": Vector_Index_Type_Enum.HnswSqlite, + "M": 16, + "EfConstruction": 200, + "DefaultEf": 50, + "IndexFile": "index.db", + "IndexFileSizeBytes": 1024000, + "EstimatedMemoryBytes": 512000, + "IsLoaded": True, + "DistanceMetric": "Cosine", + } + + +def test_get_config(mock_client, valid_vector_index_config): + """Test get_config method.""" + mock_client.request.return_value = valid_vector_index_config + mock_client.request.side_effect = None + + result = VectorIndex.get_config("test-graph-guid") + assert isinstance(result, HnswLiteVectorIndexModel) + assert result.guid == valid_vector_index_config["GUID"] + assert result.graph_guid == valid_vector_index_config["GraphGUID"] + assert result.vector_dimensionality == valid_vector_index_config["VectorDimensionality"] + assert result.m == valid_vector_index_config["M"] + + # Verify the request was made correctly + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "GET" + assert "vectorindex/config" in called_args[0][1] + + # Test without tenant_guid + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + VectorIndex.get_config("test-graph-guid") + + # Test without graph_guid + mock_client.tenant_guid = "test-tenant-guid" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + VectorIndex.get_config("") + + # Test with empty graph_guid + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + VectorIndex.get_config(None) + + +def test_get_stats(mock_client, valid_vector_index_stats): + """Test get_stats method.""" + mock_client.request.return_value = valid_vector_index_stats + mock_client.request.side_effect = None + + result = VectorIndex.get_stats("test-graph-guid") + assert isinstance(result, VectorIndexStatisticsModel) + assert result.vector_count == valid_vector_index_stats["VectorCount"] + assert result.dimensions == valid_vector_index_stats["Dimensions"] + assert result.index_type == valid_vector_index_stats["IndexType"] + assert result.m == valid_vector_index_stats["M"] + assert result.is_loaded == valid_vector_index_stats["IsLoaded"] + + # Verify the request was made correctly + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "GET" + assert "vectorindex/stats" in called_args[0][1] + + # Test without tenant_guid + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + VectorIndex.get_stats("test-graph-guid") + + # Test without graph_guid + mock_client.tenant_guid = "test-tenant-guid" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + VectorIndex.get_stats("") + + # Test with empty graph_guid + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + VectorIndex.get_stats(None) + + +def test_enable(mock_client, valid_vector_index_config): + """Test enable method.""" + config = HnswLiteVectorIndexModel(**valid_vector_index_config) + mock_client.request.return_value = valid_vector_index_config + mock_client.request.side_effect = None + + result = VectorIndex.enable("test-graph-guid", config) + assert isinstance(result, HnswLiteVectorIndexModel) + assert result.guid == valid_vector_index_config["GUID"] + assert result.vector_dimensionality == valid_vector_index_config["VectorDimensionality"] + + # Verify the request was made correctly + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "PUT" + assert "vectorindex/enable" in called_args[0][1] + assert "json" in called_args[1] + assert called_args[1]["json"]["VectorDimensionality"] == valid_vector_index_config["VectorDimensionality"] + + # Test without tenant_guid + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + VectorIndex.enable("test-graph-guid", config) + + # Test without graph_guid + mock_client.tenant_guid = "test-tenant-guid" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + VectorIndex.enable("", config) + + # Test with invalid config type + with pytest.raises(TypeError, match="Config must be an instance of HnswLiteVectorIndexModel"): + VectorIndex.enable("test-graph-guid", {"invalid": "config"}) + + # Test with None config + with pytest.raises(TypeError, match="Config must be an instance of HnswLiteVectorIndexModel"): + VectorIndex.enable("test-graph-guid", None) + + +def test_rebuild(mock_client): + """Test rebuild method.""" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + VectorIndex.rebuild("test-graph-guid") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "POST" + assert "vectorindex/rebuild" in called_args[0][1] + + # Test without tenant_guid + mock_client.request.reset_mock() + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + VectorIndex.rebuild("test-graph-guid") + + # Test without graph_guid + mock_client.tenant_guid = "test-tenant-guid" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + VectorIndex.rebuild("") + + # Test with empty graph_guid + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + VectorIndex.rebuild(None) + + +def test_delete(mock_client): + """Test delete method.""" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + VectorIndex.delete("test-graph-guid") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "vectorindex" in called_args[0][1] + + # Test without tenant_guid + mock_client.request.reset_mock() + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + VectorIndex.delete("test-graph-guid") + + # Test without graph_guid + mock_client.tenant_guid = "test-tenant-guid" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + VectorIndex.delete("") + + # Test with empty graph_guid + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + VectorIndex.delete(None) + + +def test_create_from_dict(mock_client, valid_vector_index_config): + """Test create_from_dict method.""" + mock_client.request.return_value = valid_vector_index_config + mock_client.request.side_effect = None + + result = VectorIndex.create_from_dict("test-graph-guid", valid_vector_index_config) + assert isinstance(result, HnswLiteVectorIndexModel) + assert result.guid == valid_vector_index_config["GUID"] + assert result.vector_dimensionality == valid_vector_index_config["VectorDimensionality"] + + # Verify enable was called (which calls request) + assert mock_client.request.call_count == 1 + called_args = mock_client.request.call_args + assert called_args[0][0] == "PUT" + assert "vectorindex/enable" in called_args[0][1] + + # Test without tenant_guid + mock_client.tenant_guid = None + with pytest.raises(ValueError, match="Tenant GUID is required for this resource"): + VectorIndex.create_from_dict("test-graph-guid", valid_vector_index_config) + + # Test without graph_guid + mock_client.tenant_guid = "test-tenant-guid" + with pytest.raises(ValueError, match="Graph GUID is required for this resource"): + VectorIndex.create_from_dict("", valid_vector_index_config) + + +def test_get_config_exception_handling(mock_client): + """Test get_config exception handling.""" + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("API Error") + with pytest.raises(Exception, match="API Error"): + VectorIndex.get_config("test-graph-guid") + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid response") + with pytest.raises(ValueError, match="Invalid response"): + VectorIndex.get_config("test-graph-guid") + + +def test_get_stats_exception_handling(mock_client): + """Test get_stats exception handling.""" + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("API Error") + with pytest.raises(Exception, match="API Error"): + VectorIndex.get_stats("test-graph-guid") + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid response") + with pytest.raises(ValueError, match="Invalid response"): + VectorIndex.get_stats("test-graph-guid") + + +def test_enable_exception_handling(mock_client, valid_vector_index_config): + """Test enable exception handling.""" + config = HnswLiteVectorIndexModel(**valid_vector_index_config) + + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("API Error") + with pytest.raises(Exception, match="API Error"): + VectorIndex.enable("test-graph-guid", config) + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid response") + with pytest.raises(ValueError, match="Invalid response"): + VectorIndex.enable("test-graph-guid", config) + + +def test_rebuild_exception_handling(mock_client): + """Test rebuild exception handling.""" + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("API Error") + with pytest.raises(Exception, match="API Error"): + VectorIndex.rebuild("test-graph-guid") + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid response") + with pytest.raises(ValueError, match="Invalid response"): + VectorIndex.rebuild("test-graph-guid") + + +def test_delete_exception_handling(mock_client): + """Test delete exception handling.""" + # Test when client.request raises an exception + mock_client.request.side_effect = Exception("API Error") + with pytest.raises(Exception, match="API Error"): + VectorIndex.delete("test-graph-guid") + + # Test with different exception types + mock_client.request.side_effect = ValueError("Invalid response") + with pytest.raises(ValueError, match="Invalid response"): + VectorIndex.delete("test-graph-guid") + + +def test_create_from_dict_exception_handling(mock_client, valid_vector_index_config): + """Test create_from_dict exception handling.""" + # Test when enable raises an exception + mock_client.request.side_effect = Exception("API Error") + with pytest.raises(Exception, match="API Error"): + VectorIndex.create_from_dict("test-graph-guid", valid_vector_index_config) + + # Test with invalid config_dict + with pytest.raises(Exception): # Will raise ValidationError from Pydantic + VectorIndex.create_from_dict("test-graph-guid", {"invalid": "config"}) + + +def test_hnsw_lite_vector_index_model_validation(valid_vector_index_config): + """Test HnswLiteVectorIndexModel validation.""" + model = HnswLiteVectorIndexModel(**valid_vector_index_config) + assert isinstance(model.guid, str) + assert model.graph_guid == valid_vector_index_config["GraphGUID"] + assert model.vector_dimensionality == valid_vector_index_config["VectorDimensionality"] + assert model.vector_index_type == valid_vector_index_config["VectorIndexType"] + assert model.m == valid_vector_index_config["M"] + assert model.ef_construction == valid_vector_index_config["EfConstruction"] + assert model.default_ef == valid_vector_index_config["DefaultEf"] + assert model.distance_metric == valid_vector_index_config["DistanceMetric"] + + +def test_vector_index_statistics_model_validation(valid_vector_index_stats): + """Test VectorIndexStatisticsModel validation.""" + model = VectorIndexStatisticsModel(**valid_vector_index_stats) + assert model.vector_count == valid_vector_index_stats["VectorCount"] + assert model.dimensions == valid_vector_index_stats["Dimensions"] + assert model.index_type == valid_vector_index_stats["IndexType"] + assert model.m == valid_vector_index_stats["M"] + assert model.ef_construction == valid_vector_index_stats["EfConstruction"] + assert model.default_ef == valid_vector_index_stats["DefaultEf"] + assert model.is_loaded == valid_vector_index_stats["IsLoaded"] + assert model.distance_metric == valid_vector_index_stats["DistanceMetric"] diff --git a/tests/test_models/test_vectors.py b/tests/test_models/test_vectors.py index 8f8e180..52c9770 100644 --- a/tests/test_models/test_vectors.py +++ b/tests/test_models/test_vectors.py @@ -27,13 +27,13 @@ def valid_vector_data(): """Fixture providing valid vector metadata.""" return VectorMetadataModel( guid="550e8400-e29b-41d4-a716-446655440000", - tenant_guid="550e8400-e29b-41d4-a716-446655440001", + tenant_guid="550e8400-e29b-41d4-a716-446655440001", graph_guid="550e8400-e29b-41d4-a716-446655440002", embeddings=[0.1, 0.2, 0.3], content="", dimensionality=3, created_utc="2024-01-01T00:00:00Z", - last_update_utc="2024-01-01T00:00:00Z" + last_update_utc="2024-01-01T00:00:00Z", ) @@ -57,7 +57,7 @@ def valid_search_result(valid_vector_data) -> list[VectorSearchResultModel]: edge=EdgeModel( guid="550e8400-e29b-41d4-a716-446655440004", tenant_guid="550e8400-e29b-41d4-a716-446655440001", - ) + ), ) ] @@ -73,7 +73,7 @@ def test_search_vectors_graph_domain(mock_client, valid_search_result): embeddings=embeddings, tenant_guid=tenant_guid, labels=["label1"], - tags={"key1": "value1"} + tags={"key1": "value1"}, ) assert isinstance(result, list) @@ -96,7 +96,7 @@ def test_search_vectors_node_domain(mock_client, valid_search_result): domain=VectorSearchDomainEnum.Node, embeddings=embeddings, tenant_guid=tenant_guid, - graph_guid=graph_guid + graph_guid=graph_guid, ) assert isinstance(result, list) @@ -120,7 +120,7 @@ def test_search_vectors_edge_domain(mock_client, valid_search_result): domain=VectorSearchDomainEnum.Edge, embeddings=embeddings, tenant_guid=tenant_guid, - graph_guid=graph_guid + graph_guid=graph_guid, ) assert isinstance(result, list) @@ -142,14 +142,14 @@ def test_search_vectors_missing_graph_guid(): Vector.search_vectors( domain=VectorSearchDomainEnum.Node, embeddings=embeddings, - tenant_guid=tenant_guid + tenant_guid=tenant_guid, ) with pytest.raises(ValueError, match="Graph GUID must be supplied"): Vector.search_vectors( domain=VectorSearchDomainEnum.Edge, embeddings=embeddings, - tenant_guid=tenant_guid + tenant_guid=tenant_guid, ) @@ -159,9 +159,7 @@ def test_search_vectors_empty_embeddings(): with pytest.raises(ValueError, match="must include at least one value"): Vector.search_vectors( - domain=VectorSearchDomainEnum.Graph, - embeddings=[], - tenant_guid=tenant_guid + domain=VectorSearchDomainEnum.Graph, embeddings=[], tenant_guid=tenant_guid ) @@ -177,7 +175,7 @@ def test_vector_metadata_model(): "Content": "test content", "Dimensionality": 3, "CreatedUtc": "2024-01-01T00:00:00+00:00", - "LastUpdateUtc": "2024-01-01T00:00:00+00:00" + "LastUpdateUtc": "2024-01-01T00:00:00+00:00", } model = VectorMetadataModel(**valid_data) @@ -200,7 +198,7 @@ def test_vector_search_request_model(): "Embeddings": [0.1, 0.2, 0.3], "TenantGUID": "550e8400-e29b-41d4-a716-446655440001", "Labels": ["label1"], - "Tags": {"key1": "value1"} + "Tags": {"key1": "value1"}, } model = VectorSearchRequestModel(**valid_data) @@ -220,4 +218,331 @@ def test_vector_search_result_model(valid_search_result): assert model.inner_product == 0.95 assert model.graph.guid == "550e8400-e29b-41d4-a716-446655440002" assert model.node.guid == "550e8400-e29b-41d4-a716-446655440003" - assert model.edge.guid == "550e8400-e29b-41d4-a716-446655440004" \ No newline at end of file + assert model.edge.guid == "550e8400-e29b-41d4-a716-446655440004" + + +def test_delete_all_tenant_vectors(mock_client): + """Test delete_all_tenant_vectors method.""" + mock_client.tenant_guid = "test-tenant-guid" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + Vector.delete_all_tenant_vectors() + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "tenants/test-tenant-guid/vectors/all" in called_args[0][1] + + # Test with explicit tenant_guid + mock_client.request.reset_mock() + Vector.delete_all_tenant_vectors(tenant_guid="explicit-tenant") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert "tenants/explicit-tenant/vectors/all" in called_args[0][1] + + +def test_delete_all_graph_vectors(mock_client): + """Test delete_all_graph_vectors method.""" + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = "test-graph-guid" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + Vector.delete_all_graph_vectors() + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "graphs/test-graph-guid/vectors/all" in called_args[0][1] + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + Vector.delete_all_graph_vectors( + tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert "graphs/explicit-graph/vectors/all" in called_args[0][1] + + +def test_retrieve_all_tenant_vectors(mock_client): + """Test retrieve_all_tenant_vectors method.""" + mock_client.tenant_guid = "test-tenant-guid" + test_data = [ + { + "GUID": "vector-1", + "TenantGUID": "test-tenant-guid", + "Embeddings": [0.1, 0.2, 0.3], + "Dimensionality": 3, + "CreatedUtc": "2024-01-01T00:00:00Z", + "LastUpdateUtc": "2024-01-01T00:00:00Z", + }, + { + "GUID": "vector-2", + "TenantGUID": "test-tenant-guid", + "Embeddings": [0.4, 0.5, 0.6], + "Dimensionality": 3, + "CreatedUtc": "2024-01-01T00:00:00Z", + "LastUpdateUtc": "2024-01-01T00:00:00Z", + }, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = Vector.retrieve_all_tenant_vectors() + assert isinstance(result, list) + assert len(result) == 2 + assert all(isinstance(item, VectorMetadataModel) for item in result) + assert result[0].guid == "vector-1" + assert result[1].guid == "vector-2" + + # Test with explicit tenant_guid + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = Vector.retrieve_all_tenant_vectors(tenant_guid="explicit-tenant") + assert isinstance(result, list) + assert len(result) == 2 + + +def test_retrieve_all_graph_vectors(mock_client): + """Test retrieve_all_graph_vectors method.""" + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = "test-graph-guid" + test_data = [ + { + "GUID": "vector-1", + "TenantGUID": "test-tenant-guid", + "GraphGUID": "test-graph-guid", + "Embeddings": [0.1, 0.2, 0.3], + "Dimensionality": 3, + "CreatedUtc": "2024-01-01T00:00:00Z", + "LastUpdateUtc": "2024-01-01T00:00:00Z", + }, + { + "GUID": "vector-2", + "TenantGUID": "test-tenant-guid", + "GraphGUID": "test-graph-guid", + "Embeddings": [0.4, 0.5, 0.6], + "Dimensionality": 3, + "CreatedUtc": "2024-01-01T00:00:00Z", + "LastUpdateUtc": "2024-01-01T00:00:00Z", + }, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = Vector.retrieve_all_graph_vectors() + assert isinstance(result, list) + assert len(result) == 2 + assert all(isinstance(item, VectorMetadataModel) for item in result) + assert result[0].guid == "vector-1" + assert result[1].guid == "vector-2" + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = Vector.retrieve_all_graph_vectors( + tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + assert isinstance(result, list) + assert len(result) == 2 + + +def test_retrieve_node_vectors(mock_client): + """Test retrieve_node_vectors method.""" + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = "test-graph-guid" + test_data = [ + { + "GUID": "vector-1", + "TenantGUID": "test-tenant-guid", + "GraphGUID": "test-graph-guid", + "NodeGUID": "node-guid-123", + "Embeddings": [0.1, 0.2, 0.3], + "Dimensionality": 3, + "CreatedUtc": "2024-01-01T00:00:00Z", + "LastUpdateUtc": "2024-01-01T00:00:00Z", + }, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = Vector.retrieve_node_vectors("node-guid-123") + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], VectorMetadataModel) + assert result[0].guid == "vector-1" + assert result[0].node_guid == "node-guid-123" + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = Vector.retrieve_node_vectors( + "node-guid-123", tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + assert isinstance(result, list) + assert len(result) == 1 + + +def test_retrieve_edge_vectors(mock_client): + """Test retrieve_edge_vectors method.""" + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = "test-graph-guid" + test_data = [ + { + "GUID": "vector-1", + "TenantGUID": "test-tenant-guid", + "GraphGUID": "test-graph-guid", + "EdgeGUID": "edge-guid-123", + "Embeddings": [0.1, 0.2, 0.3], + "Dimensionality": 3, + "CreatedUtc": "2024-01-01T00:00:00Z", + "LastUpdateUtc": "2024-01-01T00:00:00Z", + }, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = Vector.retrieve_edge_vectors("edge-guid-123") + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], VectorMetadataModel) + assert result[0].guid == "vector-1" + assert result[0].edge_guid == "edge-guid-123" + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = Vector.retrieve_edge_vectors( + "edge-guid-123", tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + assert isinstance(result, list) + assert len(result) == 1 + + +def test_retrieve_graph_vectors(mock_client): + """Test retrieve_graph_vectors method.""" + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = "test-graph-guid" + test_data = [ + { + "GUID": "vector-1", + "TenantGUID": "test-tenant-guid", + "GraphGUID": "test-graph-guid", + "Embeddings": [0.1, 0.2, 0.3], + "Dimensionality": 3, + "CreatedUtc": "2024-01-01T00:00:00Z", + "LastUpdateUtc": "2024-01-01T00:00:00Z", + }, + { + "GUID": "vector-2", + "TenantGUID": "test-tenant-guid", + "GraphGUID": "test-graph-guid", + "Embeddings": [0.4, 0.5, 0.6], + "Dimensionality": 3, + "CreatedUtc": "2024-01-01T00:00:00Z", + "LastUpdateUtc": "2024-01-01T00:00:00Z", + }, + ] + mock_client.request.return_value = test_data + mock_client.request.side_effect = None + + result = Vector.retrieve_graph_vectors() + assert isinstance(result, list) + assert len(result) == 2 + assert all(isinstance(item, VectorMetadataModel) for item in result) + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = Vector.retrieve_graph_vectors( + tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + assert isinstance(result, list) + assert len(result) == 2 + + # Test with include_data + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = Vector.retrieve_graph_vectors(include_data=True) + assert isinstance(result, list) + + # Test with include_subordinates + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = Vector.retrieve_graph_vectors(include_subordinates=True) + assert isinstance(result, list) + + # Test with both include parameters + mock_client.request.reset_mock() + mock_client.request.return_value = test_data + result = Vector.retrieve_graph_vectors(include_data=True, include_subordinates=True) + assert isinstance(result, list) + + +def test_delete_graph_vectors(mock_client): + """Test delete_graph_vectors method.""" + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = "test-graph-guid" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + Vector.delete_graph_vectors() + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "graphs/test-graph-guid/vectors" in called_args[0][1] + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + Vector.delete_graph_vectors( + tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert "graphs/explicit-graph/vectors" in called_args[0][1] + + +def test_delete_node_vectors(mock_client): + """Test delete_node_vectors method.""" + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = "test-graph-guid" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + Vector.delete_node_vectors("node-guid-123") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "nodes/node-guid-123/vectors" in called_args[0][1] + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + Vector.delete_node_vectors( + "node-guid-123", tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert "nodes/node-guid-123/vectors" in called_args[0][1] + + +def test_delete_edge_vectors(mock_client): + """Test delete_edge_vectors method.""" + mock_client.tenant_guid = "test-tenant-guid" + mock_client.graph_guid = "test-graph-guid" + mock_client.request.return_value = None + mock_client.request.side_effect = None + + Vector.delete_edge_vectors("edge-guid-123") + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert called_args[0][0] == "DELETE" + assert "edges/edge-guid-123/vectors" in called_args[0][1] + + # Test with explicit tenant_guid and graph_guid + mock_client.request.reset_mock() + Vector.delete_edge_vectors( + "edge-guid-123", tenant_guid="explicit-tenant", graph_guid="explicit-graph" + ) + mock_client.request.assert_called_once() + called_args = mock_client.request.call_args + assert "edges/edge-guid-123/vectors" in called_args[0][1]