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]