diff --git a/hugegraph-python-client/README.md b/hugegraph-python-client/README.md index ba59075f..8f478a64 100644 --- a/hugegraph-python-client/README.md +++ b/hugegraph-python-client/README.md @@ -10,36 +10,43 @@ pip3 install hugegraph-python ### Install from source -release soon +```bash +cd /path/to/hugegraph-python-client + +# Normal install +pip install . + +# (Optional) install the devel version +pip install -e . +``` ## Examples ```python from pyhugegraph.client import PyHugeClient -client = PyHugeClient("127.0.0.1", "8080", user="admin", pwd="admin", graph="hugegraph") - -"""system""" -print(client.get_graphinfo()) -print(client.get_all_graphs()) -print(client.get_version()) -print(client.get_graph_config()) +# For HugeGraph API version ≥ v3: (Or enable graphspace function) +# - The 'graphspace' parameter becomes relevant if graphspaces are enabled.(default name is 'DEFAULT') +# - Otherwise, the graphspace parameter is optional and can be ignored. +client = PyHugeClient("127.0.0.1", "8080", user="admin", pwd="admin", graph="hugegraph", graphspace="DEFAULT") -"""schema""" +"""" +Note: +Could refer to the official REST-API doc of your HugeGraph version for accurate details. +If some API is not as expected, please submit a issue or contact us. +""" schema = client.schema() schema.propertyKey("name").asText().ifNotExist().create() schema.propertyKey("birthDate").asText().ifNotExist().create() -schema.vertexLabel("Person").properties("name", "birthDate").usePrimaryKeyId().primaryKeys( - "name").ifNotExist().create() -schema.vertexLabel("Movie").properties("name").usePrimaryKeyId().primaryKeys( - "name").ifNotExist().create() +schema.vertexLabel("Person").properties("name", "birthDate").usePrimaryKeyId().primaryKeys("name").ifNotExist().create() +schema.vertexLabel("Movie").properties("name").usePrimaryKeyId().primaryKeys("name").ifNotExist().create() schema.edgeLabel("ActedIn").sourceLabel("Person").targetLabel("Movie").ifNotExist().create() print(schema.getVertexLabels()) print(schema.getEdgeLabels()) print(schema.getRelations()) -"""graph""" +"""Init Graph""" g = client.graph() g.addVertex("Person", {"name": "Al Pacino", "birthDate": "1940-04-25"}) g.addVertex("Person", {"name": "Robert De Niro", "birthDate": "1943-08-17"}) @@ -57,8 +64,8 @@ res = g.getVertexById("12:Al Pacino").label print(res) g.close() -"""gremlin""" +"""Execute Gremlin Query""" g = client.gremlin() -res = g.exec("g.V().limit(10)") +res = g.exec("g.V().limit(5)") print(res) ``` diff --git a/hugegraph-python-client/src/pyhugegraph/api/auth.py b/hugegraph-python-client/src/pyhugegraph/api/auth.py index 09c80b08..90b3e98d 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/auth.py +++ b/hugegraph-python-client/src/pyhugegraph/api/auth.py @@ -18,10 +18,9 @@ import json +from typing import Optional, Dict from pyhugegraph.api.common import HugeParamsBase -from pyhugegraph.utils.exceptions import NotFoundError from pyhugegraph.utils import huge_router as router -from pyhugegraph.utils.util import check_if_success class AuthManager(HugeParamsBase): @@ -29,216 +28,188 @@ class AuthManager(HugeParamsBase): @router.http("GET", "auth/users") def list_users(self, limit=None): params = {"limit": limit} if limit is not None else {} - response = self._invoke_request(params=params) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return [] + return self._invoke_request(params=params) @router.http("POST", "auth/users") - def create_user(self, user_name, user_password, user_phone=None, user_email=None): - data = { - "user_name": user_name, - "user_password": user_password, - "user_phone": user_phone, - "user_email": user_email, - } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def create_user( + self, user_name, user_password, user_phone=None, user_email=None + ) -> Optional[Dict]: + return self._invoke_request( + data=json.dumps( + { + "user_name": user_name, + "user_password": user_password, + "user_phone": user_phone, + "user_email": user_email, + } + ) + ) @router.http("DELETE", "auth/users/{user_id}") - def delete_user(self, user_id): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - if response.status_code != 204: - return response.json() - return {} + def delete_user(self, user_id) -> Optional[Dict]: # pylint: disable=unused-argument + return self._invoke_request() @router.http("PUT", "auth/users/{user_id}") def modify_user( self, - user_id, + user_id, # pylint: disable=unused-argument user_name=None, user_password=None, user_phone=None, user_email=None, - ): - data = { - "user_name": user_name, - "user_password": user_password, - "user_phone": user_phone, - "user_email": user_email, - } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + ) -> Optional[Dict]: + return self._invoke_request( + data=json.dumps( + { + "user_name": user_name, + "user_password": user_password, + "user_phone": user_phone, + "user_email": user_email, + } + ) + ) @router.http("GET", "auth/users/{user_id}") - def get_user(self, user_id): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_user(self, user_id) -> Optional[Dict]: # pylint: disable=unused-argument + return self._invoke_request() @router.http("GET", "auth/groups") - def list_groups(self, limit=None): + def list_groups(self, limit=None) -> Optional[Dict]: params = {"limit": limit} if limit is not None else {} - response = self._invoke_request(params=params) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return [] + return self._invoke_request(params=params) @router.http("POST", "auth/groups") - def create_group(self, group_name, group_description=None): + def create_group(self, group_name, group_description=None) -> Optional[Dict]: data = {"group_name": group_name, "group_description": group_description} - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request(data=json.dumps(data)) @router.http("DELETE", "auth/groups/{group_id}") - def delete_group(self, group_id): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - if response.status_code != 204: - return response.json() - return {} + def delete_group( + self, group_id # pylint: disable=unused-argument + ) -> Optional[Dict]: + return self._invoke_request() @router.http("PUT", "auth/groups/{group_id}") - def modify_group(self, group_id, group_name=None, group_description=None): + def modify_group( + self, + group_id, # pylint: disable=unused-argument + group_name=None, + group_description=None, + ) -> Optional[Dict]: data = {"group_name": group_name, "group_description": group_description} - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request(data=json.dumps(data)) @router.http("GET", "auth/groups/{group_id}") - def get_group(self, group_id): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_group(self, group_id) -> Optional[Dict]: # pylint: disable=unused-argument + return self._invoke_request() @router.http("POST", "auth/accesses") - def grant_accesses(self, group_id, target_id, access_permission): - data = { - "group": group_id, - "target": target_id, - "access_permission": access_permission, - } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def grant_accesses(self, group_id, target_id, access_permission) -> Optional[Dict]: + return self._invoke_request( + data=json.dumps( + { + "group": group_id, + "target": target_id, + "access_permission": access_permission, + } + ) + ) @router.http("DELETE", "auth/accesses/{access_id}") - def revoke_accesses(self, access_id): - response = self._invoke_request() - check_if_success(response, NotFoundError(response.content)) + def revoke_accesses( + self, access_id # pylint: disable=unused-argument + ) -> Optional[Dict]: + return self._invoke_request() @router.http("PUT", "auth/accesses/{access_id}") - def modify_accesses(self, access_id, access_description): + def modify_accesses( + self, access_id, access_description # pylint: disable=unused-argument + ) -> Optional[Dict]: # The permission of access can\'t be updated data = {"access_description": access_description} - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request(data=json.dumps(data)) @router.http("GET", "auth/accesses/{access_id}") - def get_accesses(self, access_id): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_accesses( + self, access_id # pylint: disable=unused-argument + ) -> Optional[Dict]: + return self._invoke_request() @router.http("GET", "auth/accesses") - def list_accesses(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def list_accesses(self) -> Optional[Dict]: + return self._invoke_request() @router.http("POST", "auth/targets") - def create_target(self, target_name, target_graph, target_url, target_resources): - data = { - "target_name": target_name, - "target_graph": target_graph, - "target_url": target_url, - "target_resources": target_resources, - } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def create_target( + self, target_name, target_graph, target_url, target_resources + ) -> Optional[Dict]: + return self._invoke_request( + data=json.dumps( + { + "target_name": target_name, + "target_graph": target_graph, + "target_url": target_url, + "target_resources": target_resources, + } + ) + ) @router.http("DELETE", "auth/targets/{target_id}") - def delete_target(self, target_id): - response = self._invoke_request() - check_if_success(response, NotFoundError(response.content)) + def delete_target(self, target_id) -> None: # pylint: disable=unused-argument + return self._invoke_request() @router.http("PUT", "auth/targets/{target_id}") def update_target( - self, target_id, target_name, target_graph, target_url, target_resources - ): - data = { - "target_name": target_name, - "target_graph": target_graph, - "target_url": target_url, - "target_resources": target_resources, - } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + self, + target_id, # pylint: disable=unused-argument + target_name, + target_graph, + target_url, + target_resources, + ) -> Optional[Dict]: + return self._invoke_request( + data=json.dumps( + { + "target_name": target_name, + "target_graph": target_graph, + "target_url": target_url, + "target_resources": target_resources, + } + ) + ) @router.http("GET", "auth/targets/{target_id}") - def get_target(self, target_id, response=None): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_target( + self, target_id, response=None # pylint: disable=unused-argument + ) -> Optional[Dict]: + return self._invoke_request() @router.http("GET", "auth/targets") - def list_targets(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def list_targets(self) -> Optional[Dict]: + return self._invoke_request() @router.http("POST", "auth/belongs") - def create_belong(self, user_id, group_id): + def create_belong(self, user_id, group_id) -> Optional[Dict]: data = {"user": user_id, "group": group_id} - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request(data=json.dumps(data)) @router.http("DELETE", "auth/belongs/{belong_id}") - def delete_belong(self, belong_id): - response = self._invoke_request() - check_if_success(response, NotFoundError(response.content)) + def delete_belong(self, belong_id) -> None: # pylint: disable=unused-argument + return self._invoke_request() @router.http("PUT", "auth/belongs/{belong_id}") - def update_belong(self, belong_id, description): + def update_belong( + self, belong_id, description # pylint: disable=unused-argument + ) -> Optional[Dict]: data = {"belong_description": description} - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request(data=json.dumps(data)) @router.http("GET", "auth/belongs/{belong_id}") - def get_belong(self, belong_id): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_belong( + self, belong_id # pylint: disable=unused-argument + ) -> Optional[Dict]: + return self._invoke_request() @router.http("GET", "auth/belongs") - def list_belongs(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def list_belongs(self) -> Optional[Dict]: + return self._invoke_request() diff --git a/hugegraph-python-client/src/pyhugegraph/api/common.py b/hugegraph-python-client/src/pyhugegraph/api/common.py index 731d4d2d..631a2779 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/common.py +++ b/hugegraph-python-client/src/pyhugegraph/api/common.py @@ -16,8 +16,11 @@ # under the License. +import re + from abc import ABC -from pyhugegraph.utils.huge_router import HGraphRouter +from pyhugegraph.utils.log import log +from pyhugegraph.utils.huge_router import RouterMixin from pyhugegraph.utils.huge_requests import HGraphSession @@ -49,12 +52,24 @@ def __init__(self, sess: HGraphSession) -> None: def close(self): self._sess.close() + @property + def session(self): + """ + Get session. + + Returns: + ------- + HGraphSession: session + """ + return self._sess + # todo: rename -> HGraphModule | HGraphRouterable | HGraphModel -class HugeParamsBase(HGraphContext, HGraphRouter): +class HugeParamsBase(HGraphContext, RouterMixin): def __init__(self, sess: HGraphSession) -> None: super().__init__(sess) self._parameter_holder = None + self.__camel_to_snake_case() def add_parameter(self, key, value): self._parameter_holder.set(key, value) @@ -67,3 +82,21 @@ def create_parameter_holder(self): def clean_parameter_holder(self): self._parameter_holder = None + + def __camel_to_snake_case(self): + camel_case_pattern = re.compile(r"^[a-z]+([A-Z][a-z]*)+$") + attributes = dir(self) + for attr in attributes: + if attr.startswith("__"): + continue + if not callable(getattr(self, attr)): + continue + if camel_case_pattern.match(attr): + s = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", attr) + snake = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s).lower() + setattr(self, snake, getattr(self, attr)) + log.debug( # pylint: disable=logging-fstring-interpolation + f"The method {self.__class__.__name__}.{attr} " + f"is deprecated and will be removed in future versions. " + f"Please update your code to use the new method name {self.__class__.__name__}.{snake} instead." + ) diff --git a/hugegraph-python-client/src/pyhugegraph/api/graph.py b/hugegraph-python-client/src/pyhugegraph/api/graph.py index 708d63c8..f4104044 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/graph.py +++ b/hugegraph-python-client/src/pyhugegraph/api/graph.py @@ -17,21 +17,12 @@ import json +from typing import Optional, List from pyhugegraph.api.common import HugeParamsBase from pyhugegraph.structure.vertex_data import VertexData from pyhugegraph.structure.edge_data import EdgeData from pyhugegraph.utils import huge_router as router -from pyhugegraph.utils.exceptions import ( - NotFoundError, - CreateError, - RemoveError, - UpdateError, -) -from pyhugegraph.utils.util import ( - create_exception, - check_if_authorized, - check_if_success, -) +from pyhugegraph.utils.exceptions import NotFoundError class GraphManager(HugeParamsBase): @@ -43,12 +34,8 @@ def addVertex(self, label, properties, id=None): data["id"] = id data["label"] = label data["properties"] = properties - response = self._invoke_request(data=json.dumps(data)) - if check_if_success( - response, CreateError(f"create vertex failed: {str(response.content)}") - ): - res = VertexData(json.loads(response.content)) - return res + if response := self._invoke_request(data=json.dumps(data)): + return VertexData(response) return None @router.http("POST", "graph/vertices/batch") @@ -56,46 +43,28 @@ def addVertices(self, input_data): data = [] for item in input_data: data.append({"label": item[0], "properties": item[1]}) - response = self._invoke_request(data=json.dumps(data)) - if check_if_success( - response, CreateError(f"create vertexes failed: {str(response.content)}") - ): - res = [] - for item in json.loads(response.content): - res.append(VertexData({"id": item})) - return res + if response := self._invoke_request(data=json.dumps(data)): + return [VertexData({"id": item}) for item in response] return None @router.http("PUT", 'graph/vertices/"{vertex_id}"?action=append') - def appendVertex(self, vertex_id, properties): + def appendVertex(self, vertex_id, properties): # pylint: disable=unused-argument data = {"properties": properties} - response = self._invoke_request(data=json.dumps(data)) - if check_if_success( - response, UpdateError(f"append vertex failed: {str(response.content)}") - ): - res = VertexData(json.loads(response.content)) - return res + if response := self._invoke_request(data=json.dumps(data)): + return VertexData(response) return None @router.http("PUT", 'graph/vertices/"{vertex_id}"?action=eliminate') - def eliminateVertex(self, vertex_id, properties): + def eliminateVertex(self, vertex_id, properties): # pylint: disable=unused-argument data = {"properties": properties} - response = self._invoke_request(data=json.dumps(data)) - if check_if_success( - response, UpdateError(f"eliminate vertex failed: {str(response.content)}") - ): - res = VertexData(json.loads(response.content)) - return res + if response := self._invoke_request(data=json.dumps(data)): + return VertexData(response) return None @router.http("GET", 'graph/vertices/"{vertex_id}"') - def getVertexById(self, vertex_id): - response = self._invoke_request() - if check_if_success( - response, NotFoundError(f"Vertex not found: {str(response.content)}") - ): - res = VertexData(json.loads(response.content)) - return res + def getVertexById(self, vertex_id): # pylint: disable=unused-argument + if response := self._invoke_request(): + return VertexData(response) return None def getVertexByPage(self, label, limit, page=None, properties=None): @@ -110,16 +79,11 @@ def getVertexByPage(self, label, limit, page=None, properties=None): para += "&page" para = para + "&limit=" + str(limit) path = path + para[1:] - response = self._sess.request(path) - if check_if_success( - response, NotFoundError(f"Vertex not found: {str(response.content)}") - ): - res = [] - for item in json.loads(response.content)["vertices"]: - res.append(VertexData(item)) - next_page = json.loads(response.content)["page"] + if response := self._sess.request(path): + res = [VertexData(item) for item in response["vertices"]] + next_page = response["page"] return res, next_page - return None + return None, None def getVertexByCondition(self, label="", limit=0, page=None, properties=None): path = "graph/vertices?" @@ -135,43 +99,28 @@ def getVertexByCondition(self, label="", limit=0, page=None, properties=None): else: para += "&page" path = path + para[1:] - response = self._sess.request(path) - if check_if_success( - response, NotFoundError(f"Vertex not found: {str(response.content)}") - ): - res = [] - for item in json.loads(response.content)["vertices"]: - res.append(VertexData(item)) - return res + if response := self._sess.request(path): + return [VertexData(item) for item in response["vertices"]] return None @router.http("DELETE", 'graph/vertices/"{vertex_id}"') - def removeVertexById(self, vertex_id): - response = self._invoke_request() - if check_if_success( - response, RemoveError(f"remove vertex failed: {str(response.content)}") - ): - return response.content - return None + def removeVertexById(self, vertex_id): # pylint: disable=unused-argument + return self._invoke_request() @router.http("POST", "graph/edges") - def addEdge(self, edge_label, out_id, in_id, properties): + def addEdge(self, edge_label, out_id, in_id, properties) -> Optional[EdgeData]: data = { "label": edge_label, "outV": out_id, "inV": in_id, "properties": properties, } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success( - response, CreateError(f"created edge failed: {str(response.content)}") - ): - res = EdgeData(json.loads(response.content)) - return res + if response := self._invoke_request(data=json.dumps(data)): + return EdgeData(response) return None @router.http("POST", "graph/edges/batch") - def addEdges(self, input_data): + def addEdges(self, input_data) -> Optional[List[EdgeData]]: data = [] for item in input_data: data.append( @@ -184,44 +133,36 @@ def addEdges(self, input_data): "properties": item[5], } ) - response = self._invoke_request(data=json.dumps(data)) - if check_if_success( - response, CreateError(f"created edges failed: {str(response.content)}") - ): - res = [] - for item in json.loads(response.content): - res.append(EdgeData({"id": item})) - return res + if response := self._invoke_request(data=json.dumps(data)): + return [EdgeData({"id": item}) for item in response] return None @router.http("PUT", "graph/edges/{edge_id}?action=append") - def appendEdge(self, edge_id, properties): - response = self._invoke_request(data=json.dumps({"properties": properties})) - if check_if_success( - response, UpdateError(f"append edge failed: {str(response.content)}") + def appendEdge( + self, edge_id, properties # pylint: disable=unused-argument + ) -> Optional[EdgeData]: + if response := self._invoke_request( + data=json.dumps({"properties": properties}) ): - res = EdgeData(json.loads(response.content)) - return res + return EdgeData(response) return None @router.http("PUT", "graph/edges/{edge_id}?action=eliminate") - def eliminateEdge(self, edge_id, properties): - response = self._invoke_request(data=json.dumps({"properties": properties})) - if check_if_success( - response, UpdateError(f"eliminate edge failed: {str(response.content)}") + def eliminateEdge( + self, edge_id, properties # pylint: disable=unused-argument + ) -> Optional[EdgeData]: + if response := self._invoke_request( + data=json.dumps({"properties": properties}) ): - res = EdgeData(json.loads(response.content)) - return res + return EdgeData(response) return None @router.http("GET", "graph/edges/{edge_id}") - def getEdgeById(self, edge_id): - response = self._invoke_request() - if check_if_success( - response, NotFoundError(f"not found edge: {str(response.content)}") - ): - res = EdgeData(json.loads(response.content)) - return res + def getEdgeById( + self, edge_id # pylint: disable=unused-argument + ) -> Optional[EdgeData]: + if response := self._invoke_request(): + return EdgeData(response) return None def getEdgeByPage( @@ -233,7 +174,7 @@ def getEdgeByPage( page=None, properties=None, ): - path = f"graph/edges?" + path = "graph/edges?" para = "" if vertex_id: if direction: @@ -251,53 +192,32 @@ def getEdgeByPage( if limit > 0: para = para + "&limit=" + str(limit) path = path + para[1:] - response = self._sess.request(path) - if check_if_success( - response, NotFoundError(f"not found edges: {str(response.content)}") - ): - res = [] - for item in json.loads(response.content)["edges"]: - res.append(EdgeData(item)) - return res, json.loads(response.content)["page"] - return None + if response := self._sess.request(path): + return [EdgeData(item) for item in response["edges"]], response["page"] + return None, None @router.http("DELETE", "graph/edges/{edge_id}") - def removeEdgeById(self, edge_id): - response = self._invoke_request() - if check_if_success( - response, RemoveError(f"remove edge failed: {str(response.content)}") - ): - return response.content - return None + def removeEdgeById(self, edge_id) -> dict: # pylint: disable=unused-argument + return self._invoke_request() - def getVerticesById(self, vertex_ids): + def getVerticesById(self, vertex_ids) -> Optional[List[VertexData]]: if not vertex_ids: return [] path = "traversers/vertices?" for vertex_id in vertex_ids: path += f'ids="{vertex_id}"&' path = path.rstrip("&") - response = self._sess.request(path) - if response.status_code == 200 and check_if_authorized(response): - res = [] - for item in json.loads(response.content)["vertices"]: - res.append(VertexData(item)) - return res - create_exception(response.content) + if response := self._sess.request(path): + return [VertexData(item) for item in response["vertices"]] return None - def getEdgesById(self, edge_ids): + def getEdgesById(self, edge_ids) -> Optional[List[EdgeData]]: if not edge_ids: return [] path = "traversers/edges?" for vertex_id in edge_ids: path += f"ids={vertex_id}&" path = path.rstrip("&") - response = self._sess.request(path) - if response.status_code == 200 and check_if_authorized(response): - res = [] - for item in json.loads(response.content)["edges"]: - res.append(EdgeData(item)) - return res - create_exception(response.content) + if response := self._sess.request(path): + return [EdgeData(item) for item in response["edges"]] return None diff --git a/hugegraph-python-client/src/pyhugegraph/api/graphs.py b/hugegraph-python-client/src/pyhugegraph/api/graphs.py index c7f4e303..dbcf477a 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/graphs.py +++ b/hugegraph-python-client/src/pyhugegraph/api/graphs.py @@ -19,49 +19,39 @@ from pyhugegraph.api.common import HugeParamsBase from pyhugegraph.utils import huge_router as router -from pyhugegraph.utils.exceptions import NotFoundError -from pyhugegraph.utils.util import check_if_success +from pyhugegraph.utils.util import ResponseValidation class GraphsManager(HugeParamsBase): @router.http("GET", "/graphs") - def get_all_graphs(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return str(response.content) - return "" + def get_all_graphs(self) -> dict: + return self._invoke_request(validator=ResponseValidation("text")) @router.http("GET", "/versions") - def get_version(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return str(response.content) - return "" + def get_version(self) -> dict: + return self._invoke_request(validator=ResponseValidation("text")) @router.http("GET", "") - def get_graph_info(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return str(response.content) - return "" + def get_graph_info(self) -> dict: + return self._invoke_request(validator=ResponseValidation("text")) - def clear_graph_all_data(self): - if self._sess._cfg.gs_supported: + def clear_graph_all_data(self) -> dict: + if self._sess.cfg.gs_supported: response = self._sess.request( - "", "PUT", data=json.dumps({"action": "clear", "clear_schema": True}) + "", + "PUT", + validator=ResponseValidation("text"), + data=json.dumps({"action": "clear", "clear_schema": True}), ) else: response = self._sess.request( - "clear?confirm_message=I%27m+sure+to+delete+all+data", "DELETE" + "clear?confirm_message=I%27m+sure+to+delete+all+data", + "DELETE", + validator=ResponseValidation("text"), ) - if check_if_success(response, NotFoundError(response.content)): - return str(response.content) - return "" + return response @router.http("GET", "conf") - def get_graph_config(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return str(response.content) - return "" + def get_graph_config(self) -> dict: + return self._invoke_request(validator=ResponseValidation("text")) diff --git a/hugegraph-python-client/src/pyhugegraph/api/gremlin.py b/hugegraph-python-client/src/pyhugegraph/api/gremlin.py index 3095642d..e02e7fb2 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/gremlin.py +++ b/hugegraph-python-client/src/pyhugegraph/api/gremlin.py @@ -15,14 +15,13 @@ # specific language governing permissions and limitations # under the License. -import json from pyhugegraph.api.common import HugeParamsBase +from pyhugegraph.utils.exceptions import NotFoundError from pyhugegraph.structure.gremlin_data import GremlinData from pyhugegraph.structure.response_data import ResponseData -from pyhugegraph.utils.exceptions import NotFoundError from pyhugegraph.utils import huge_router as router -from pyhugegraph.utils.util import check_if_success +from pyhugegraph.utils.log import log class GremlinManager(HugeParamsBase): @@ -30,18 +29,23 @@ class GremlinManager(HugeParamsBase): @router.http("POST", "/gremlin") def exec(self, gremlin): gremlin_data = GremlinData(gremlin) - if self._sess._cfg.gs_supported: + if self._sess.cfg.gs_supported: gremlin_data.aliases = { - "graph": f"{self._sess._cfg.graphspace}-{self._sess._cfg.graph_name}", - "g": f"__g_{self._sess._cfg.graphspace}-{self._sess._cfg.graph_name}", + "graph": f"{self._sess.cfg.graphspace}-{self._sess.cfg.graph_name}", + "g": f"__g_{self._sess.cfg.graphspace}-{self._sess.cfg.graph_name}", } else: gremlin_data.aliases = { - "graph": f"{self._sess._cfg.graph_name}", - "g": f"__g_{self._sess._cfg.graph_name}", + "graph": f"{self._sess.cfg.graph_name}", + "g": f"__g_{self._sess.cfg.graph_name}", } - response = self._invoke_request(data=gremlin_data.to_json()) - error = NotFoundError(f"Gremlin can't get results: {str(response.content)}") - if check_if_success(response, error): - return ResponseData(json.loads(response.content)).result - return "" + + try: + if response := self._invoke_request(data=gremlin_data.to_json()): + return ResponseData(response).result + log.error( # pylint: disable=logging-fstring-interpolation + f"Gremlin can't get results: {str(response)}" + ) + return None + except Exception as e: + raise NotFoundError(f"Gremlin can't get results: {e}") from e diff --git a/hugegraph-python-client/src/pyhugegraph/api/metric.py b/hugegraph-python-client/src/pyhugegraph/api/metric.py index 9d27d523..27fd715c 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/metric.py +++ b/hugegraph-python-client/src/pyhugegraph/api/metric.py @@ -16,72 +16,43 @@ # under the License. from pyhugegraph.api.common import HugeParamsBase -from pyhugegraph.utils.exceptions import NotFoundError from pyhugegraph.utils import huge_router as router -from pyhugegraph.utils.util import check_if_success class MetricsManager(HugeParamsBase): @router.http("GET", "/metrics/?type=json") - def get_all_basic_metrics(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_all_basic_metrics(self) -> dict: + return self._invoke_request() @router.http("GET", "/metrics/gauges") - def get_gauges_metrics(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_gauges_metrics(self) -> dict: + return self._invoke_request() @router.http("GET", "/metrics/counters") - def get_counters_metrics(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_counters_metrics(self) -> dict: + return self._invoke_request() @router.http("GET", "/metrics/gauges") - def get_histograms_metrics(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_histograms_metrics(self) -> dict: + return self._invoke_request() @router.http("GET", "/metrics/meters") - def get_meters_metrics(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_meters_metrics(self) -> dict: + return self._invoke_request() @router.http("GET", "/metrics/timers") - def get_timers_metrics(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_timers_metrics(self) -> dict: + return self._invoke_request() @router.http("GET", "/metrics/statistics/?type=json") - def get_statistics_metrics(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_statistics_metrics(self) -> dict: + return self._invoke_request() @router.http("GET", "/metrics/system") - def get_system_metrics(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_system_metrics(self) -> dict: + return self._invoke_request() @router.http("GET", "/metrics/backend") - def get_backend_metrics(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get_backend_metrics(self) -> dict: + return self._invoke_request() diff --git a/hugegraph-python-client/src/pyhugegraph/api/rank.py b/hugegraph-python-client/src/pyhugegraph/api/rank.py new file mode 100644 index 00000000..ba7bb0bb --- /dev/null +++ b/hugegraph-python-client/src/pyhugegraph/api/rank.py @@ -0,0 +1,65 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +from pyhugegraph.utils import huge_router as router +from pyhugegraph.structure.rank_data import ( + PersonalRankParameters, + NeighborRankParameters, +) +from pyhugegraph.api.common import HugeParamsBase + + +class RankManager(HugeParamsBase): + """ + This class provides methods to interact with the rank APIs in HugeGraphServer. + It allows for personalized recommendations based on graph traversal and ranking algorithms. + + Methods: + personal_rank(source, label, alpha=0.85, max_degree=10000, max_depth=5, + limit=100, sorted=True, with_label="BOTH_LABEL"): + Computes the Personal Rank for a given source vertex and edge label. + + neighbor_rank(source, steps, alpha=0.85, capacity=10000000): + Computes the Neighbor Rank for a given source vertex and defined steps. + """ + + @router.http("POST", "traversers/personalrank") + def personal_rank(self, body_params: PersonalRankParameters): + """ + Computes the Personal Rank for a given source vertex and edge label. + + Args: + body_params (PersonalRankParameters): BodyParams defines the body parameters for the rank API requests. + + Returns: + dict: A dictionary containing the ranked list of vertices and their corresponding rank values. + """ + return self._invoke_request(data=body_params.dumps()) + + @router.http("POST", "traversers/neighborrank") + def neighbor_rank(self, body_params: NeighborRankParameters): + """ + Computes the Neighbor Rank for a given source vertex and defined steps. + + Args: + body_params (NeighborRankParameters): BodyParams defines the body parameters for the rank API requests. + + Returns: + dict: A dictionary containing the probability of reaching other vertices from the source vertex. + """ + return self._invoke_request(data=body_params.dumps()) diff --git a/hugegraph-python-client/src/pyhugegraph/api/rebuild.py b/hugegraph-python-client/src/pyhugegraph/api/rebuild.py new file mode 100644 index 00000000..e0b4d64b --- /dev/null +++ b/hugegraph-python-client/src/pyhugegraph/api/rebuild.py @@ -0,0 +1,87 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +from pyhugegraph.utils import huge_router as router +from pyhugegraph.api.common import HugeParamsBase + + +class RebuildManager(HugeParamsBase): + """ + Manages the rebuilding of index, vertex, and edge labels via HTTP endpoints. + + Methods: + rebuild_indexlabels(indexlabel: str): + Rebuilds the specified IndexLabel. Returns a dictionary with the task ID. + + rebuild_vertexlabels(vertexlabel: str): + Rebuilds the specified VertexLabel. Returns a dictionary with the task ID. + + rebuild_edgelabels(edgelabel: str): + Rebuilds the specified EdgeLabel. Returns a dictionary with the task ID. + """ + + @router.http("PUT", "jobs/rebuild/indexlabels/{indexlabel}") + def rebuild_indexlabels(self, indexlabel: str): # pylint: disable=unused-argument + """ + Rebuild IndexLabel. + + Args: + indexlabel (str): Name of the indexlabel. + + Returns: + dict: A dictionary containing the response from the HTTP request. + The structure of the response is as follows: + response = { + "task_id": 1 # Unique identifier for the task. + } + """ + return self._invoke_request() + + @router.http("PUT", "jobs/rebuild/vertexlabels/{vertexlabel}") + def rebuild_vertexlabels(self, vertexlabel: str): # pylint: disable=unused-argument + """ + Rebuild VertexLabel. + + Args: + vertexlabel (str): Name of the vertexlabel. + + Returns: + dict: A dictionary containing the response from the HTTP request. + The structure of the response is as follows: + response = { + "task_id": 1 # Unique identifier for the task. + } + """ + return self._invoke_request() + + @router.http("PUT", "jobs/rebuild/edgelabels/{edgelabel}") + def rebuild_edgelabels(self, edgelabel: str): # pylint: disable=unused-argument + """ + Rebuild EdgeLabel. + + Args: + edgelabel (str): Name of the edgelabel. + + Returns: + dict: A dictionary containing the response from the HTTP request. + The structure of the response is as follows: + response = { + "task_id": 1 # Unique identifier for the task. + } + """ + return self._invoke_request() diff --git a/hugegraph-python-client/src/pyhugegraph/api/schema.py b/hugegraph-python-client/src/pyhugegraph/api/schema.py index ab92bc83..fdebc9d4 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/schema.py +++ b/hugegraph-python-client/src/pyhugegraph/api/schema.py @@ -16,9 +16,7 @@ # under the License. -import json - -from typing import Optional, Dict +from typing import Optional, Dict, List from pyhugegraph.api.common import HugeParamsBase from pyhugegraph.api.schema_manage.edge_label import EdgeLabel from pyhugegraph.api.schema_manage.index_label import IndexLabel @@ -28,9 +26,8 @@ from pyhugegraph.structure.index_label_data import IndexLabelData from pyhugegraph.structure.property_key_data import PropertyKeyData from pyhugegraph.structure.vertex_label_data import VertexLabelData -from pyhugegraph.utils.exceptions import NotFoundError -from pyhugegraph.utils.util import check_if_success from pyhugegraph.utils import huge_router as router +from pyhugegraph.utils.log import log class SchemaManager(HugeParamsBase): @@ -66,93 +63,73 @@ def indexLabel(self, name): index_label.add_parameter("name", name) return index_label - @router.http("GET", "schema?format={format}") - def getSchema(self, format: str = "json") -> Optional[Dict]: - response = self._invoke_request() - error = NotFoundError(f"schema not found: {str(response.content)}") - if check_if_success(response, error): - schema = json.loads(response.content) - return schema - return None + @router.http("GET", "schema?format={_format}") + def getSchema( + self, _format: str = "json" # pylint: disable=unused-argument + ) -> Optional[Dict]: + return self._invoke_request() @router.http("GET", "schema/propertykeys/{property_name}") - def getPropertyKey(self, property_name): - response = self._invoke_request() - error = NotFoundError(f"PropertyKey not found: {str(response.content)}") - if check_if_success(response, error): - property_keys_data = PropertyKeyData(json.loads(response.content)) - return property_keys_data + def getPropertyKey( + self, property_name # pylint: disable=unused-argument + ) -> Optional[PropertyKeyData]: + if response := self._invoke_request(): + return PropertyKeyData(response) return None @router.http("GET", "schema/propertykeys") - def getPropertyKeys(self): - response = self._invoke_request() - res = [] - if check_if_success(response): - for item in json.loads(response.content)["propertykeys"]: - res.append(PropertyKeyData(item)) - return res + def getPropertyKeys(self) -> Optional[List[PropertyKeyData]]: + if response := self._invoke_request(): + return [PropertyKeyData(item) for item in response["propertykeys"]] return None @router.http("GET", "schema/vertexlabels/{name}") - def getVertexLabel(self, name): - response = self._invoke_request() - error = NotFoundError(f"VertexLabel not found: {str(response.content)}") - if check_if_success(response, error): - res = VertexLabelData(json.loads(response.content)) - return res + def getVertexLabel( + self, name # pylint: disable=unused-argument + ) -> Optional[VertexLabelData]: + if response := self._invoke_request(): + return VertexLabelData(response) + log.error("VertexLabel not found: %s", str(response)) return None @router.http("GET", "schema/vertexlabels") - def getVertexLabels(self): - response = self._invoke_request() - res = [] - if check_if_success(response): - for item in json.loads(response.content)["vertexlabels"]: - res.append(VertexLabelData(item)) - return res + def getVertexLabels(self) -> Optional[List[VertexLabelData]]: + if response := self._invoke_request(): + return [VertexLabelData(item) for item in response["vertexlabels"]] + return None @router.http("GET", "schema/edgelabels/{label_name}") - def getEdgeLabel(self, label_name: str): - response = self._invoke_request() - error = NotFoundError(f"EdgeLabel not found: {str(response.content)}") - if check_if_success(response, error): - res = EdgeLabelData(json.loads(response.content)) - return res + def getEdgeLabel( + self, label_name: str # pylint: disable=unused-argument + ) -> Optional[EdgeLabelData]: + if response := self._invoke_request(): + return EdgeLabelData(response) + log.error("EdgeLabel not found: %s", str(response)) return None @router.http("GET", "schema/edgelabels") - def getEdgeLabels(self): - response = self._invoke_request() - res = [] - if check_if_success(response): - for item in json.loads(response.content)["edgelabels"]: - res.append(EdgeLabelData(item)) - return res + def getEdgeLabels(self) -> Optional[List[EdgeLabelData]]: + if response := self._invoke_request(): + return [EdgeLabelData(item) for item in response["edgelabels"]] + return None @router.http("GET", "schema/edgelabels") - def getRelations(self): - response = self._invoke_request() - res = [] - if check_if_success(response): - for item in json.loads(response.content)["edgelabels"]: - res.append(EdgeLabelData(item).relations()) - return res + def getRelations(self) -> Optional[List[str]]: + if response := self._invoke_request(): + return [EdgeLabelData(item).relations() for item in response["edgelabels"]] + return None @router.http("GET", "schema/indexlabels/{name}") - def getIndexLabel(self, name): - response = self._invoke_request() - error = NotFoundError(f"EdgeLabel not found: {str(response.content)}") - if check_if_success(response, error): - res = IndexLabelData(json.loads(response.content)) - return res + def getIndexLabel( + self, name # pylint: disable=unused-argument + ) -> Optional[IndexLabelData]: + if response := self._invoke_request(): + return IndexLabelData(response) + log.error("IndexLabel not found: %s", str(response)) return None @router.http("GET", "schema/indexlabels") - def getIndexLabels(self): - response = self._invoke_request() - res = [] - if check_if_success(response): - for item in json.loads(response.content)["indexlabels"]: - res.append(IndexLabelData(item)) - return res + def getIndexLabels(self) -> Optional[List[IndexLabelData]]: + if response := self._invoke_request(): + return [IndexLabelData(item) for item in response["indexlabels"]] + return None diff --git a/hugegraph-python-client/src/pyhugegraph/api/schema_manage/edge_label.py b/hugegraph-python-client/src/pyhugegraph/api/schema_manage/edge_label.py index ee6b8c1c..37b85649 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/schema_manage/edge_label.py +++ b/hugegraph-python-client/src/pyhugegraph/api/schema_manage/edge_label.py @@ -18,9 +18,9 @@ import json from pyhugegraph.api.common import HugeParamsBase -from pyhugegraph.utils.exceptions import CreateError, UpdateError, RemoveError +from pyhugegraph.utils.util import ResponseValidation from pyhugegraph.utils.huge_decorator import decorator_params, decorator_create -from pyhugegraph.utils.util import check_if_success, check_if_authorized +from pyhugegraph.utils.log import log class EdgeLabel(HugeParamsBase): @@ -81,8 +81,7 @@ def nullableKeys(self, *args) -> "EdgeLabel": @decorator_params def ifNotExist(self) -> "EdgeLabel": path = f'schema/edgelabels/{self._parameter_holder.get_value("name")}' - response = self._sess.request(path) - if response.status_code == 200 and check_if_authorized(response): + if _ := self._sess.request(path, validator=ResponseValidation(strict=False)): self._parameter_holder.set("not_exist", False) return self @@ -105,25 +104,19 @@ def create(self): if key in dic: data[key] = dic[key] path = "schema/edgelabels" - response = self._sess.request(path, "POST", data=json.dumps(data)) self.clean_parameter_holder() - error = CreateError( - f'CreateError: "create EdgeLabel failed", Detail: "{str(response.content)}"' - ) - if check_if_success(response, error): - return f'create EdgeLabel success, Detail: "{str(response.content)}"' + if response := self._sess.request(path, "POST", data=json.dumps(data)): + return f'create EdgeLabel success, Detail: "{str(response)}"' + log.error(f'create EdgeLabel failed, Detail: "{str(response)}"') return None @decorator_params def remove(self): path = f'schema/edgelabels/{self._parameter_holder.get_value("name")}' - response = self._sess.request(path, "DELETE") self.clean_parameter_holder() - error = RemoveError( - f'RemoveError: "remove EdgeLabel failed", Detail: "{str(response.content)}"' - ) - if check_if_success(response, error): - return f'remove EdgeLabel success, Detail: "{str(response.content)}"' + if response := self._sess.request(path, "DELETE"): + return f'remove EdgeLabel success, Detail: "{str(response)}"' + log.error(f'remove EdgeLabel failed, Detail: "{str(response)}"') return None @decorator_params @@ -136,13 +129,10 @@ def append(self): data[key] = dic[key] path = f'schema/edgelabels/{data["name"]}?action=append' - response = self._sess.request(path, "PUT", data=json.dumps(data)) self.clean_parameter_holder() - error = UpdateError( - f'UpdateError: "append EdgeLabel failed", Detail: "{str(response.content)}"' - ) - if check_if_success(response, error): - return f'append EdgeLabel success, Detail: "{str(response.content)}"' + if response := self._sess.request(path, "PUT", data=json.dumps(data)): + return f'append EdgeLabel success, Detail: "{str(response)}"' + log.error(f'append EdgeLabel failed, Detail: "{str(response)}"') return None @decorator_params @@ -155,11 +145,8 @@ def eliminate(self): ) path = f"schema/edgelabels/{name}?action=eliminate" data = {"name": name, "user_data": user_data} - response = self._sess.request(path, "PUT", data=json.dumps(data)) self.clean_parameter_holder() - error = UpdateError( - f'UpdateError: "eliminate EdgeLabel failed", Detail: "{str(response.content)}"' - ) - if check_if_success(response, error): - return f'eliminate EdgeLabel success, Detail: "{str(response.content)}"' + if response := self._sess.request(path, "PUT", data=json.dumps(data)): + return f'eliminate EdgeLabel success, Detail: "{str(response)}"' + log.error(f'eliminate EdgeLabel failed, Detail: "{str(response)}"') return None diff --git a/hugegraph-python-client/src/pyhugegraph/api/schema_manage/index_label.py b/hugegraph-python-client/src/pyhugegraph/api/schema_manage/index_label.py index 09ee8aa1..1437177e 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/schema_manage/index_label.py +++ b/hugegraph-python-client/src/pyhugegraph/api/schema_manage/index_label.py @@ -19,9 +19,9 @@ from pyhugegraph.api.common import HugeParamsBase +from pyhugegraph.utils.util import ResponseValidation from pyhugegraph.utils.huge_decorator import decorator_params, decorator_create -from pyhugegraph.utils.exceptions import CreateError, RemoveError -from pyhugegraph.utils.util import check_if_authorized, check_if_success +from pyhugegraph.utils.log import log class IndexLabel(HugeParamsBase): @@ -58,15 +58,24 @@ def range(self) -> "IndexLabel": return self @decorator_params - def Search(self) -> "IndexLabel": + def search(self) -> "IndexLabel": self._parameter_holder.set("index_type", "SEARCH") return self + @decorator_params + def shard(self) -> "IndexLabel": + self._parameter_holder.set("index_type", "SHARD") + return self + + @decorator_params + def unique(self) -> "IndexLabel": + self._parameter_holder.set("index_type", "UNIQUE") + return self + @decorator_params def ifNotExist(self) -> "IndexLabel": path = f'schema/indexlabels/{self._parameter_holder.get_value("name")}' - response = self._sess.request(path) - if response.status_code == 200 and check_if_authorized(response): + if _ := self._sess.request(path, validator=ResponseValidation(strict=False)): self._parameter_holder.set("not_exist", False) return self @@ -79,25 +88,19 @@ def create(self): data["base_value"] = dic["base_value"] data["index_type"] = dic["index_type"] data["fields"] = list(dic["fields"]) - path = f"schema/indexlabels" - response = self._sess.request(path, "POST", data=json.dumps(data)) + path = "schema/indexlabels" self.clean_parameter_holder() - error = CreateError( - f'CreateError: "create IndexLabel failed", Detail "{str(response.content)}"' - ) - if check_if_success(response, error): - return f'create IndexLabel success, Deatil: "{str(response.content)}"' + if response := self._sess.request(path, "POST", data=json.dumps(data)): + return f'create IndexLabel success, Detail: "{str(response)}"' + log.error(f'create IndexLabel failed, Detail: "{str(response)}"') return None @decorator_params def remove(self): name = self._parameter_holder.get_value("name") path = f"schema/indexlabels/{name}" - response = self._sess.request(path, "DELETE") self.clean_parameter_holder() - error = RemoveError( - f'RemoveError: "remove IndexLabel failed", Detail "{str(response.content)}"' - ) - if check_if_success(response, error): - return f'remove IndexLabel success, Deatil: "{str(response.content)}"' + if response := self._sess.request(path, "DELETE"): + return f'remove IndexLabel success, Detail: "{str(response)}"' + log.error(f'remove IndexLabel failed, Detail: "{str(response)}"') return None diff --git a/hugegraph-python-client/src/pyhugegraph/api/schema_manage/property_key.py b/hugegraph-python-client/src/pyhugegraph/api/schema_manage/property_key.py index e6a1765f..38f5bd9e 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/schema_manage/property_key.py +++ b/hugegraph-python-client/src/pyhugegraph/api/schema_manage/property_key.py @@ -19,9 +19,9 @@ from pyhugegraph.api.common import HugeParamsBase -from pyhugegraph.utils.exceptions import CreateError, UpdateError, RemoveError +from pyhugegraph.utils.util import ResponseValidation from pyhugegraph.utils.huge_decorator import decorator_params, decorator_create -from pyhugegraph.utils.util import check_if_success, check_if_authorized +from pyhugegraph.utils.log import log class PropertyKey(HugeParamsBase): @@ -100,8 +100,7 @@ def userdata(self, *args) -> "PropertyKey": def ifNotExist(self) -> "PropertyKey": path = f'schema/propertykeys/{self._parameter_holder.get_value("name")}' - response = self._sess.request(path) - if response.status_code == 200 and check_if_authorized(response): + if _ := self._sess.request(path, validator=ResponseValidation(strict=False)): self._parameter_holder.set("not_exist", False) return self @@ -114,16 +113,10 @@ def create(self): if "cardinality" in dic: property_keys["cardinality"] = dic["cardinality"] path = "schema/propertykeys" - response = self._sess.request(path, "POST", data=json.dumps(property_keys)) self.clean_parameter_holder() - if check_if_success( - response, - CreateError( - f'CreateError: "create PropertyKey failed", Detail: {str(response.content)}' - ), - ): - return f"create PropertyKey success, Detail: {str(response.content)}" - return f"create PropertyKey failed, Detail: {str(response.content)}" + if response := self._sess.request(path, "POST", data=json.dumps(property_keys)): + return f"create PropertyKey success, Detail: {str(response)}" + log.error("create PropertyKey failed, Detail: %s", str(response)) @decorator_params def append(self): @@ -134,16 +127,10 @@ def append(self): data = {"name": property_name, "user_data": user_data} path = f"schema/propertykeys/{property_name}/?action=append" - response = self._sess.request(path, "PUT", data=json.dumps(data)) self.clean_parameter_holder() - if check_if_success( - response, - UpdateError( - f'UpdateError: "append PropertyKey failed", Detail: {str(response.content)}' - ), - ): - return f"append PropertyKey success, Detail: {str(response.content)}" - return f"append PropertyKey failed, Detail: {str(response.content)}" + if response := self._sess.request(path, "PUT", data=json.dumps(data)): + return f"append PropertyKey success, Detail: {str(response)}" + log.error("append PropertyKey failed, Detail: %s", str(response)) @decorator_params def eliminate(self): @@ -154,26 +141,16 @@ def eliminate(self): data = {"name": property_name, "user_data": user_data} path = f"schema/propertykeys/{property_name}/?action=eliminate" - response = self._sess.request(path, "PUT", data=json.dumps(data)) self.clean_parameter_holder() - error = UpdateError( - f'UpdateError: "eliminate PropertyKey failed", Detail: {str(response.content)}' - ) - if check_if_success(response, error): - return f"eliminate PropertyKey success, Detail: {str(response.content)}" - return f"eliminate PropertyKey failed, Detail: {str(response.content)}" + if response := self._sess.request(path, "PUT", data=json.dumps(data)): + return f"eliminate PropertyKey success, Detail: {str(response)}" + log.error("eliminate PropertyKey failed, Detail: %s", str(response)) @decorator_params def remove(self): dic = self._parameter_holder.get_dic() path = f'schema/propertykeys/{dic["name"]}' - response = self._sess.request(path, "DELETE") self.clean_parameter_holder() - if check_if_success( - response, - RemoveError( - f'RemoveError: "delete PropertyKey failed", Detail: {str(response.content)}' - ), - ): - return f'delete PropertyKey success, Detail: {dic["name"]}' - return f"delete PropertyKey failed, Detail: {str(response.content)}" + if response := self._sess.request(path, "DELETE"): + return f"delete PropertyKey success, Detail: {str(response)}" + log.error("delete PropertyKey failed, Detail: %s", str(response)) diff --git a/hugegraph-python-client/src/pyhugegraph/api/schema_manage/vertex_label.py b/hugegraph-python-client/src/pyhugegraph/api/schema_manage/vertex_label.py index a9b49e8e..610d214c 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/schema_manage/vertex_label.py +++ b/hugegraph-python-client/src/pyhugegraph/api/schema_manage/vertex_label.py @@ -18,9 +18,9 @@ import json from pyhugegraph.api.common import HugeParamsBase -from pyhugegraph.utils.exceptions import CreateError, UpdateError, RemoveError +from pyhugegraph.utils.util import ResponseValidation from pyhugegraph.utils.huge_decorator import decorator_params, decorator_create -from pyhugegraph.utils.util import check_if_success, check_if_authorized +from pyhugegraph.utils.log import log class VertexLabel(HugeParamsBase): @@ -78,8 +78,7 @@ def userdata(self, *args) -> "VertexLabel": def ifNotExist(self) -> "VertexLabel": path = f'schema/vertexlabels/{self._parameter_holder.get_value("name")}' - response = self._sess.request(path) - if response.status_code == 200 and check_if_authorized(response): + if _ := self._sess.request(path, validator=ResponseValidation(strict=False)): self._parameter_holder.set("not_exist", False) return self @@ -100,15 +99,11 @@ def create(self): for key in key_list: if key in dic: data[key] = dic[key] - path = f"schema/vertexlabels" - response = self._sess.request(path, "POST", data=json.dumps(data)) + path = "schema/vertexlabels" self.clean_parameter_holder() - error = CreateError( - f'CreateError: "create VertexLabel failed", Detail: "{str(response.content)}"' - ) - if check_if_success(response, error): - return f'create VertexLabel success, Detail: "{str(response.content)}"' - return f'create VertexLabel failed, Detail: "{str(response.content)}"' + if response := self._sess.request(path, "POST", data=json.dumps(data)): + return f'create VertexLabel success, Detail: "{str(response)}"' + log.error("create VertexLabel failed, Detail: %s", str(response)) @decorator_params def append(self): @@ -123,27 +118,19 @@ def append(self): "nullable_keys": nullable_keys, "user_data": user_data, } - response = self._sess.request(path, "PUT", data=json.dumps(data)) self.clean_parameter_holder() - error = UpdateError( - f'UpdateError: "append VertexLabel failed", Detail: "{str(response.content)}"' - ) - if check_if_success(response, error): - return f'append VertexLabel success, Detail: "{str(response.content)}"' - return f'append VertexLabel failed, Detail: "{str(response.content)}"' + if response := self._sess.request(path, "PUT", data=json.dumps(data)): + return f'append VertexLabel success, Detail: "{str(response)}"' + log.error("append VertexLabel failed, Detail: %s", str(response)) @decorator_params def remove(self): name = self._parameter_holder.get_value("name") path = f"schema/vertexlabels/{name}" - response = self._sess.request(path, "DELETE") self.clean_parameter_holder() - error = RemoveError( - f'RemoveError: "remove VertexLabel failed", Detail: "{str(response.content)}"' - ) - if check_if_success(response, error): - return f'remove VertexLabel success, Detail: "{str(response.content)}"' - return f'remove VertexLabel failed, Detail: "{str(response.content)}"' + if response := self._sess.request(path, "DELETE"): + return f'remove VertexLabel success, Detail: "{str(response)}"' + log.error("remove VertexLabel failed, Detail: %s", str(response)) @decorator_params def eliminate(self): @@ -156,10 +143,6 @@ def eliminate(self): "name": self._parameter_holder.get_value("name"), "user_data": user_data, } - response = self._sess.request(path, "PUT", data=json.dumps(data)) - error = UpdateError( - f'UpdateError: "eliminate VertexLabel failed", Detail: "{str(response.content)}"' - ) - if check_if_success(response, error): - return f'eliminate VertexLabel success, Detail: "{str(response.content)}"' - return f'eliminate VertexLabel failed, Detail: "{str(response.content)}"' + if response := self._sess.request(path, "PUT", data=json.dumps(data)): + return f'eliminate VertexLabel success, Detail: "{str(response)}"' + log.error("eliminate VertexLabel failed, Detail: %s", str(response)) diff --git a/hugegraph-python-client/src/pyhugegraph/api/services.py b/hugegraph-python-client/src/pyhugegraph/api/services.py new file mode 100644 index 00000000..0c2c0a79 --- /dev/null +++ b/hugegraph-python-client/src/pyhugegraph/api/services.py @@ -0,0 +1,135 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +from pyhugegraph.utils import huge_router as router +from pyhugegraph.api.common import HugeParamsBase +from pyhugegraph.structure.services_data import ServiceCreateParameters + + +class ServicesManager(HugeParamsBase): + """ + Manages services within HugeGraph via HTTP endpoints. + + This class acts as an interface to manage different types of services such as online + transaction processing (OLTP) and online analytical processing (OLAP) + in HugeGraph. It currently supports dynamic creation and management of services, + with a focus on OLTP services for now. + + Methods: + create_service(graphspace: str, service_create_parameters: ServiceCreateParameters): + Creates a new service within a specified graph space using the provided parameters. + Returns a dictionary with the details of the created service. + + list_services(graphspace: str): + Lists all services available within a specified graph space. + Returns a dictionary containing a list of service names. + + get_service(graphspace: str, service: str): + Retrieves detailed information about a specific service within a graph space. + Returns a dictionary with the service details. + + delete_service(graphspace: str, service: str): + Deletes a specific service within a graph space after confirmation. + No return value expected; the operation's success is indicated by an HTTP 204 status code. + """ + + @router.http("POST", "/graphspaces/{graphspace}/services") + def create_services( + self, + graphspace: str, # pylint: disable=unused-argument + body_params: ServiceCreateParameters, + ): + """ + Create HugeGraph Servers. + + Args: + service_create (ServiceCreate): The name of the service. + + Returns: + dict: A dictionary containing the response from the HTTP request. + """ + return self._invoke_request(data=body_params.dumps()) + + @router.http("GET", "/graphspaces/${graphspace}/services") + def list_services(self, graphspace: str): # pylint: disable=unused-argument + """ + List all services in a specified graph space.. + + Args: + graphspace (str): The name of the graph space to list services from. + + + Response: + A list of service names in the specified graph space. + + Returns: + dict: A dictionary containing the list of service names. + Example: + { + "services": ["service1", "service2"] + } + """ + return self._invoke_request() + + @router.http("GET", "/graphspaces/{graphspace}/services/{service}") + def get_service( + self, graphspace: str, service: str # pylint: disable=unused-argument + ): + """ + Retrieve the details of a specific service. + + Args: + graphspace (str): The name of the graph space where the service is located. + service (str): The name of the service to retrieve details for. + + Response: + A dictionary containing the details of the specified service. + + Returns: + dict: A dictionary with the service details. + Example: + { + "name": "service1", + "description": "This is a description of service1.", + "type": "OLTP", + // ... other service details + } + """ + return self._invoke_request() + + def delete_service( + self, graphspace: str, service: str # pylint: disable=unused-argument + ): + """ + Delete a specific service within a graph space. + + Args: + graphspace (str): The name of the graph space where the service is located. + service (str): The name of the service to be deleted. + + Response: + 204 + + Returns: + None + """ + return self._sess.request( + f"/graphspaces/{graphspace}/services/{service}" + f"?confirm_message=I'm sure to delete the service", + "DELETE", + ) diff --git a/hugegraph-python-client/src/pyhugegraph/api/task.py b/hugegraph-python-client/src/pyhugegraph/api/task.py index c31eb8df..0668eb0c 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/task.py +++ b/hugegraph-python-client/src/pyhugegraph/api/task.py @@ -16,9 +16,7 @@ # under the License. from pyhugegraph.api.common import HugeParamsBase -from pyhugegraph.utils.exceptions import NotFoundError from pyhugegraph.utils import huge_router as router -from pyhugegraph.utils.util import check_if_success class TaskManager(HugeParamsBase): @@ -30,24 +28,16 @@ def list_tasks(self, status=None, limit=None): params["status"] = status if limit is not None: params["limit"] = limit - response = self._invoke_request(params=params) - check_if_success(response, NotFoundError(response.content)) - return response.json() + return self._invoke_request(params=params) @router.http("GET", "tasks/{task_id}") - def get_task(self, task_id): - response = self._invoke_request() - check_if_success(response, NotFoundError(response.content)) - return response.json() + def get_task(self, task_id): # pylint: disable=unused-argument + return self._invoke_request() @router.http("DELETE", "tasks/{task_id}") - def delete_task(self, task_id): - response = self._invoke_request() - check_if_success(response, NotFoundError(response.content)) - return response.status_code + def delete_task(self, task_id): # pylint: disable=unused-argument + return self._invoke_request() @router.http("PUT", "tasks/{task_id}?action=cancel") - def cancel_task(self, task_id): - response = self._invoke_request() - check_if_success(response, NotFoundError(response.content)) - return response.json() + def cancel_task(self, task_id): # pylint: disable=unused-argument + return self._invoke_request() diff --git a/hugegraph-python-client/src/pyhugegraph/api/traverser.py b/hugegraph-python-client/src/pyhugegraph/api/traverser.py index 165178bc..f9dbbb21 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/traverser.py +++ b/hugegraph-python-client/src/pyhugegraph/api/traverser.py @@ -17,86 +17,71 @@ import json from pyhugegraph.api.common import HugeParamsBase -from pyhugegraph.utils.exceptions import NotFoundError from pyhugegraph.utils import huge_router as router -from pyhugegraph.utils.util import check_if_success class TraverserManager(HugeParamsBase): @router.http("GET", 'traversers/kout?source="{source_id}"&max_depth={max_depth}') - def k_out(self, source_id, max_depth): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def k_out(self, source_id, max_depth): # pylint: disable=unused-argument + return self._invoke_request() @router.http( "GET", 'traversers/kneighbor?source="{source_id}"&max_depth={max_depth}' ) - def k_neighbor(self, source_id, max_depth): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def k_neighbor(self, source_id, max_depth): # pylint: disable=unused-argument + return self._invoke_request() @router.http( "GET", 'traversers/sameneighbors?vertex="{vertex_id}"&other="{other_id}"' ) - def same_neighbors(self, vertex_id, other_id): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def same_neighbors(self, vertex_id, other_id): # pylint: disable=unused-argument + return self._invoke_request() @router.http( "GET", 'traversers/jaccardsimilarity?vertex="{vertex_id}"&other="{other_id}"' ) - def jaccard_similarity(self, vertex_id, other_id): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def jaccard_similarity( + self, vertex_id, other_id # pylint: disable=unused-argument + ): + return self._invoke_request() @router.http( "GET", 'traversers/shortestpath?source="{source_id}"&target="{target_id}"&max_depth={max_depth}', ) - def shortest_path(self, source_id, target_id, max_depth): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def shortest_path( + self, source_id, target_id, max_depth # pylint: disable=unused-argument + ): + return self._invoke_request() @router.http( "GET", 'traversers/allshortestpaths?source="{source_id}"&target="{target_id}"&max_depth={max_depth}', ) - def all_shortest_paths(self, source_id, target_id, max_depth): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def all_shortest_paths( + self, source_id, target_id, max_depth # pylint: disable=unused-argument + ): + return self._invoke_request() @router.http( "GET", - 'traversers/weightedshortestpath?source="{source_id}"&target="{target_id}"&weight={weight}&max_depth={max_depth}', + 'traversers/weightedshortestpath?source="{source_id}"&target="{target_id}"' + "&weight={weight}&max_depth={max_depth}", ) - def weighted_shortest_path(self, source_id, target_id, weight, max_depth): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def weighted_shortest_path( + self, source_id, target_id, weight, max_depth # pylint: disable=unused-argument + ): + return self._invoke_request() @router.http( "GET", 'traversers/singlesourceshortestpath?source="{source_id}"&max_depth={max_depth}', ) - def single_source_shortest_path(self, source_id, max_depth): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def single_source_shortest_path( + self, source_id, max_depth # pylint: disable=unused-argument + ): + return self._invoke_request() @router.http("POST", "traversers/multinodeshortestpath") def multi_node_shortest_path( @@ -110,71 +95,67 @@ def multi_node_shortest_path( ): if properties is None: properties = {} - data = { - "vertices": {"ids": vertices}, - "step": {"direction": direction, "properties": properties}, - "max_depth": max_depth, - "capacity": capacity, - "with_vertex": with_vertex, - } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request( + data=json.dumps( + { + "vertices": {"ids": vertices}, + "step": {"direction": direction, "properties": properties}, + "max_depth": max_depth, + "capacity": capacity, + "with_vertex": with_vertex, + } + ) + ) @router.http( "GET", 'traversers/paths?source="{source_id}"&target="{target_id}"&max_depth={max_depth}', ) - def paths(self, source_id, target_id, max_depth): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def paths(self, source_id, target_id, max_depth): # pylint: disable=unused-argument + return self._invoke_request() @router.http("POST", "traversers/customizedpaths") def customized_paths( self, sources, steps, sort_by="INCR", with_vertex=True, capacity=-1, limit=-1 ): - data = { - "sources": sources, - "steps": steps, - "sort_by": sort_by, - "with_vertex": with_vertex, - "capacity": capacity, - "limit": limit, - } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request( + data=json.dumps( + { + "sources": sources, + "steps": steps, + "sort_by": sort_by, + "with_vertex": with_vertex, + "capacity": capacity, + "limit": limit, + } + ) + ) @router.http("POST", "traversers/templatepaths") def template_paths( self, sources, targets, steps, capacity=10000, limit=10, with_vertex=True ): - data = { - "sources": sources, - "targets": targets, - "steps": steps, - "capacity": capacity, - "limit": limit, - "with_vertex": with_vertex, - } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request( + data=json.dumps( + { + "sources": sources, + "targets": targets, + "steps": steps, + "capacity": capacity, + "limit": limit, + "with_vertex": with_vertex, + } + ) + ) @router.http( "GET", 'traversers/crosspoints?source="{source_id}"&target="{target_id}"&max_depth={max_depth}', ) - def crosspoints(self, source_id, target_id, max_depth): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def crosspoints( + self, source_id, target_id, max_depth # pylint: disable=unused-argument + ): + return self._invoke_request() @router.http("POST", "traversers/customizedcrosspoints") def customized_crosspoints( @@ -186,32 +167,26 @@ def customized_crosspoints( capacity=-1, limit=-1, ): - data = { - "sources": sources, - "path_patterns": path_patterns, - "with_path": with_path, - "with_vertex": with_vertex, - "capacity": capacity, - "limit": limit, - } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request( + data=json.dumps( + { + "sources": sources, + "path_patterns": path_patterns, + "with_path": with_path, + "with_vertex": with_vertex, + "capacity": capacity, + "limit": limit, + } + ) + ) @router.http("GET", 'traversers/rings?source="{source_id}"&max_depth={max_depth}') - def rings(self, source_id, max_depth): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def rings(self, source_id, max_depth): # pylint: disable=unused-argument + return self._invoke_request() @router.http("GET", 'traversers/rays?source="{source_id}"&max_depth={max_depth}') - def rays(self, source_id, max_depth): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def rays(self, source_id, max_depth): # pylint: disable=unused-argument + return self._invoke_request() @router.http("POST", "traversers/fusiformsimilarity") def fusiform_similarity( @@ -231,39 +206,33 @@ def fusiform_similarity( with_intermediary=False, with_vertex=True, ): - data = { - "sources": sources, - "label": label, - "direction": direction, - "min_neighbors": min_neighbors, - "alpha": alpha, - "min_similars": min_similars, - "top": top, - "group_property": group_property, - "min_groups": min_groups, - "max_degree": max_degree, - "capacity": capacity, - "limit": limit, - "with_intermediary": with_intermediary, - "with_vertex": with_vertex, - } - response = self._invoke_request(data=json.dumps(data)) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request( + data=json.dumps( + { + "sources": sources, + "label": label, + "direction": direction, + "min_neighbors": min_neighbors, + "alpha": alpha, + "min_similars": min_similars, + "top": top, + "group_property": group_property, + "min_groups": min_groups, + "max_degree": max_degree, + "capacity": capacity, + "limit": limit, + "with_intermediary": with_intermediary, + "with_vertex": with_vertex, + } + ) + ) @router.http("GET", "traversers/vertices") def vertices(self, ids): params = {"ids": f'"{ids}"'} - response = self._invoke_request(params=params) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request(params=params) @router.http("GET", "traversers/edges") def edges(self, ids): params = {"ids": ids} - response = self._invoke_request(params=params) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request(params=params) diff --git a/hugegraph-python-client/src/pyhugegraph/api/variable.py b/hugegraph-python-client/src/pyhugegraph/api/variable.py index 4c060b9f..c5fe8401 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/variable.py +++ b/hugegraph-python-client/src/pyhugegraph/api/variable.py @@ -18,35 +18,23 @@ import json from pyhugegraph.api.common import HugeParamsBase -from pyhugegraph.utils.exceptions import NotFoundError from pyhugegraph.utils import huge_router as router -from pyhugegraph.utils.util import check_if_success class VariableManager(HugeParamsBase): @router.http("PUT", "variables/{key}") - def set(self, key, value): - response = self._invoke_request(data=json.dumps({"data": value})) - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def set(self, key, value): # pylint: disable=unused-argument + return self._invoke_request(data=json.dumps({"data": value})) @router.http("GET", "variables/{key}") - def get(self, key): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + def get(self, key): # pylint: disable=unused-argument + return self._invoke_request() @router.http("GET", "variables") def all(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request() @router.http("DELETE", "variables/{key}") - def remove(self, key): - response = self._invoke_request() - check_if_success(response, NotFoundError(response.content)) + def remove(self, key): # pylint: disable=unused-argument + return self._invoke_request() diff --git a/hugegraph-python-client/src/pyhugegraph/api/version.py b/hugegraph-python-client/src/pyhugegraph/api/version.py index 23b5baf1..3635c771 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/version.py +++ b/hugegraph-python-client/src/pyhugegraph/api/version.py @@ -16,16 +16,11 @@ # under the License. from pyhugegraph.api.common import HugeParamsBase -from pyhugegraph.utils.exceptions import NotFoundError from pyhugegraph.utils import huge_router as router -from pyhugegraph.utils.util import check_if_success class VersionManager(HugeParamsBase): @router.http("GET", "/versions") def version(self): - response = self._invoke_request() - if check_if_success(response, NotFoundError(response.content)): - return response.json() - return {} + return self._invoke_request() diff --git a/hugegraph-python-client/src/pyhugegraph/client.py b/hugegraph-python-client/src/pyhugegraph/client.py index fd776ff1..b740e89f 100644 --- a/hugegraph-python-client/src/pyhugegraph/client.py +++ b/hugegraph-python-client/src/pyhugegraph/client.py @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -from typing import Optional +from typing import Any, Callable, Optional, TypeVar from pyhugegraph.api.auth import AuthManager from pyhugegraph.api.graph import GraphManager @@ -30,13 +30,15 @@ from pyhugegraph.utils.huge_config import HGraphConfig from pyhugegraph.utils.huge_requests import HGraphSession +T = TypeVar("T") -def manager_builder(fn): + +def manager_builder(fn: Callable[[Any, "HGraphSession"], T]) -> Callable[[Any], T]: attr_name = "_lazy_" + fn.__name__ - def wrapper(self: "PyHugeClient"): + def wrapper(self: "PyHugeClient") -> T: if not hasattr(self, attr_name): - session = HGraphSession(self._cfg) + session = HGraphSession(self.cfg) setattr(self, attr_name, fn(self)(session)) return getattr(self, attr_name) @@ -51,10 +53,10 @@ def __init__( graph: str, user: str, pwd: str, + graphspace: Optional[str] = None, timeout: int = 10, - gs: Optional[str] = None, ): - self._cfg = HGraphConfig(ip, port, user, pwd, graph, gs, timeout) + self.cfg = HGraphConfig(ip, port, user, pwd, graph, graphspace, timeout) @manager_builder def schema(self) -> "SchemaManager": @@ -97,4 +99,4 @@ def version(self) -> "VersionManager": return VersionManager def __repr__(self) -> str: - return f"{self._cfg}" + return f"{self.cfg}" diff --git a/hugegraph-python-client/src/pyhugegraph/structure/rank_data.py b/hugegraph-python-client/src/pyhugegraph/structure/rank_data.py new file mode 100644 index 00000000..e4adc323 --- /dev/null +++ b/hugegraph-python-client/src/pyhugegraph/structure/rank_data.py @@ -0,0 +1,96 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import json + +from typing import List, Union +from dataclasses import asdict, dataclass, field + + +@dataclass +class NeighborRankStep: + """ + Steps object defines the traversal path rules from the starting vertex. + """ + + direction: str = "BOTH" + labels: List[str] = field(default_factory=list) + max_degree: int = 10000 + top: int = 100 + + def dumps(self): + return json.dumps(asdict(self)) + + +@dataclass +class NeighborRankParameters: + """ + BodyParams defines the body parameters for the rank API requests. + """ + + source: Union[str, int] + label: str + alpha: float = 0.85 + capacity: int = 10000000 + steps: List[NeighborRankStep] = field(default_factory=list) + + def dumps(self): + return json.dumps(asdict(self)) + + +@dataclass +class PersonalRankParameters: + """ + Data class that represents the body parameters for a rank API request. + + Attributes: + - source (Union[str, int]): The ID of the source vertex. This is a required field with no default value. + - label (str): The label of the edge type that starts the traversal. + This is a required field with no default value. + - alpha (float): The probability of moving to a neighboring vertex in each iteration, + similar to the alpha in PageRank. Optional with a default value of 0.85. + The value should be in the range (0, 1]. + - max_degree (int): The maximum number of adjacent edges a single vertex can traverse + during the query process. Optional with a default value of 10000. + The value should be greater than 0. + - max_depth (int): The maximum number of iterations for the traversal. Optional with a default value of 5. + The value should be within the range [2, 50]. + - limit (int): The maximum number of vertices to return in the results. Optional with a default value of 100. + The value should be greater than 0. + - max_diff (float): The precision difference for early convergence (to be implemented later). + Optional with a default value of 0.0001. The value should be in the range (0, 1). + - sorted (bool): Indicates whether the results should be sorted based on rank. + If true, the results are sorted in descending order of rank; + if false, they are not sorted. Optional with a default value of True. + - with_label (str): Determines which results to keep in the final output. + Optional with a default value of "BOTH_LABEL". The options are "SAME_LABEL" to keep only + vertices of the same category as the source vertex, "OTHER_LABEL" to keep only vertices + of a different category (the other end of a bipartite graph), and "BOTH_LABEL" to keep both. + """ + + source: Union[str, int] + label: str + alpha: float = 0.85 + max_degree: int = 10000 + max_depth: int = 5 + limit: int = 100 + max_diff: float = 0.0001 + sorted: bool = True + with_label: str = "BOTH_LABEL" + + def dumps(self): + return json.dumps(asdict(self)) diff --git a/hugegraph-python-client/src/pyhugegraph/structure/services_data.py b/hugegraph-python-client/src/pyhugegraph/structure/services_data.py new file mode 100644 index 00000000..07b7a23f --- /dev/null +++ b/hugegraph-python-client/src/pyhugegraph/structure/services_data.py @@ -0,0 +1,62 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import json + +from typing import List, Optional +from dataclasses import asdict, dataclass, field + + +@dataclass +class ServiceCreateParameters: + """ + Data class representing the request body for HugeGraph services. + + Attributes: + - name (str): The name of the service. It must consist of lowercase letters, numbers, and underscores. + The first character must be a lowercase letter and the length must not exceed 48. + - description (str): A description of the service. + - type (str): The type of service. Currently, only 'OLTP' is allowed. Default is 'OLTP'. + - count (int): The number of HugeGraphServer instances. Must be greater than 0. Default is 1. + - cpu_limit (int): The number of CPU cores per HugeGraphServer instance. Must be greater than 0. Default is 1. + - memory_limit (int): The memory size per HugeGraphServer instance in GB. Must be greater than 0. Default is 4. + - storage_limit (int): The disk size for HStore in GB. Must be greater than 0. Default is 100. + - route_type (str): Required when deployment_type is 'K8S'. + Accepted values are 'ClusterIP', 'LoadBalancer', 'NodePort'. + - port (int): Required when deployment_type is 'K8S'. Must be greater than 0. + Default is None and invalid for other deployment types. + - urls (List[str]): Required when deployment_type is 'MANUAL'. + Should not be provided for other deployment types. + - deployment_type (str): The deployment type of the service. + 'K8S' indicates service deployment through a Kubernetes cluster, + 'MANUAL' indicates manual service deployment. Default is an empty string. + """ + + name: str + description: str + type: str = "OLTP" + count: int = 1 + cpu_limit: int = 1 + memory_limit: int = 4 + storage_limit: int = 100 + route_type: Optional[str] = None + port: Optional[int] = None + urls: List[str] = field(default_factory=list) + deployment_type: Optional[str] = None + + def dumps(self): + return json.dumps(asdict(self)) diff --git a/hugegraph-python-client/src/pyhugegraph/utils/huge_config.py b/hugegraph-python-client/src/pyhugegraph/utils/huge_config.py index f7e00d97..fc5e1c7c 100644 --- a/hugegraph-python-client/src/pyhugegraph/utils/huge_config.py +++ b/hugegraph-python-client/src/pyhugegraph/utils/huge_config.py @@ -21,6 +21,7 @@ from dataclasses import dataclass, field from typing import Optional, List +from pyhugegraph.utils.log import log @dataclass @@ -46,22 +47,24 @@ def __post_init__(self): f"http://{self.ip}:{self.port}/versions", timeout=1 ) core = response.json()["versions"]["core"] - print(f"Retrieved API version information from the server: {core}.") + log.info( # pylint: disable=logging-fstring-interpolation + f"Retrieved API version information from the server: {core}." + ) - match = re.search("(\d+)\.(\d+)(?:\.(\d+))?(?:\.\d+)?", core) + match = re.search(r"(\d+)\.(\d+)(?:\.(\d+))?(?:\.\d+)?", core) major, minor, patch = map(int, match.groups()) self.version.extend([major, minor, patch]) if major >= 3: self.graphspace = "DEFAULT" self.gs_supported = True - print( - f"graph space is not set, default value 'DEFAULT' will be used." + log.warning( + "graph space is not set, default value 'DEFAULT' will be used." ) - except Exception as e: + except Exception as e: # pylint: disable=broad-exception-caught traceback.print_exception(e) self.gs_supported = False - print( + log.warning( "Failed to retrieve API version information from the server, reverting to default v1." ) diff --git a/hugegraph-python-client/src/pyhugegraph/utils/huge_decorator.py b/hugegraph-python-client/src/pyhugegraph/utils/huge_decorator.py index ecc94bf4..a6dbe891 100644 --- a/hugegraph-python-client/src/pyhugegraph/utils/huge_decorator.py +++ b/hugegraph-python-client/src/pyhugegraph/utils/huge_decorator.py @@ -17,7 +17,6 @@ from decorator import decorator - from pyhugegraph.utils.exceptions import NotAuthorizedError diff --git a/hugegraph-python-client/src/pyhugegraph/utils/huge_requests.py b/hugegraph-python-client/src/pyhugegraph/utils/huge_requests.py index 83ececea..e983fcbe 100644 --- a/hugegraph-python-client/src/pyhugegraph/utils/huge_requests.py +++ b/hugegraph-python-client/src/pyhugegraph/utils/huge_requests.py @@ -15,14 +15,17 @@ # specific language governing permissions and limitations # under the License. -import requests -from requests.adapters import HTTPAdapter + from urllib3.util.retry import Retry from urllib.parse import urljoin - from typing import Any, Optional +from requests.adapters import HTTPAdapter from pyhugegraph.utils.constants import Constants from pyhugegraph.utils.huge_config import HGraphConfig +from pyhugegraph.utils.util import ResponseValidation +from pyhugegraph.utils.log import log + +import requests class HGraphSession: @@ -66,11 +69,25 @@ def __configure_session(self): self._session.mount("http://", adapter) self._session.mount("https://", adapter) self._session.keep_alive = False - # logger.debug( - # "Session configured with retries=%s and backoff_factor=%s", - # self.retries, - # self.backoff_factor, - # ) + log.debug( + "Session configured with retries=%s and backoff_factor=%s", + self._retries, + self._backoff_factor, + ) + + @property + def cfg(self): + """ + Get the configuration information of the current instance. + + Args: + None. + + Returns: + ------- + HGraphConfig: The configuration information of the current instance. + """ + return self._cfg def resolve(self, path: str): """ @@ -80,7 +97,8 @@ def resolve(self, path: str): :return: The fully resolved URL as a string. When path is "/some/things": - - Since path starts with "/", it is considered an absolute path, and urljoin will replace the path part of the base URL. + - Since path starts with "/", it is considered an absolute path, + and urljoin will replace the path part of the base URL. - Assuming the base URL is "http://127.0.0.1:8000/graphspaces/default/graphs/test_graph/" - The result will be "http://127.0.0.1:8000/some/things" @@ -101,18 +119,34 @@ def resolve(self, path: str): return urljoin(url, path).strip("/") def close(self): + """ + closes the session. + + Args: + None + + Returns: + None + + """ self._session.close() - def request(self, path: str, method: str = "GET", **kwargs: Any): - try: - # print(method, self.resolve(path)) - response = getattr(self._session, method.lower())( - self.resolve(path), - auth=self._auth, - headers=self._headers, - timeout=self._timeout, - **kwargs, - ) - return response - except requests.RequestException as e: - raise + def request( + self, + path: str, + method: str = "GET", + validator=ResponseValidation(), + **kwargs: Any, + ) -> dict: + url = self.resolve(path) + response: requests.Response = getattr(self._session, method.lower())( + url, + auth=self._auth, + headers=self._headers, + timeout=self._timeout, + **kwargs, + ) + log.debug( # pylint: disable=logging-fstring-interpolation + f"Request: {method} {url} validator={validator} kwargs={kwargs} {response}" + ) + return validator(response, method=method, path=path) diff --git a/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py b/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py index 08f93118..0db81ed1 100644 --- a/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py +++ b/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py @@ -16,19 +16,21 @@ # under the License. import re -import json import inspect import functools import threading -from abc import ABC -from typing import Any, Callable, Dict, TYPE_CHECKING +from dataclasses import dataclass +from typing import Any, Callable, Dict, Optional, TYPE_CHECKING +from pyhugegraph.utils.log import log +from pyhugegraph.utils.util import ResponseValidation + if TYPE_CHECKING: from pyhugegraph.api.common import HGraphContext -class SingletonBase(type): +class SingletonMeta(type): _instances = {} _lock = threading.Lock() @@ -44,18 +46,50 @@ def __call__(cls, *args, **kwargs): return cls._instances[cls] -class HGraphRouterManager(metaclass=SingletonBase): +@dataclass +class Route: + method: str + path: str + request_func: Optional[Callable] = None + + +class RouterRegistry(metaclass=SingletonMeta): def __init__(self): - self._routers = {} + self._routers: Dict[str, Route] = {} - def register(self, key, path): - self._routers.update({key: path}) + def register(self, key: str, route: Route): + self._routers[key] = route - def get_routers(self): + @property + def routers(self): return self._routers def __repr__(self) -> str: - return json.dumps(self._routers, indent=4) + return str(self._routers) + + +def register(method: str, path: str) -> Callable: + + def decorator(func: Callable) -> Callable: + RouterRegistry().register( + func.__qualname__, + Route(method, path), + ) + + @functools.wraps(func) + def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any: + route = RouterRegistry().routers.get(func.__qualname__) + + if route.request_func is None: + route.request_func = functools.partial( + self.session.request, method=method + ) + + return func(self, *args, **kwargs) + + return wrapper + + return decorator def http(method: str, path: str) -> Callable: @@ -72,7 +106,7 @@ def http(method: str, path: str) -> Callable: def decorator(func: Callable) -> Callable: """Decorator function that modifies the original function.""" - HGraphRouterManager().register(func.__qualname__, (method, path)) + RouterRegistry().register(func.__qualname__, Route(method, path)) @functools.wraps(func) def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any: @@ -99,10 +133,10 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any: else: formatted_path = path - # todo: If possible, reduce unnecessary multiple creations. - cache_key = (func.__qualname__, method, formatted_path) # Use functools.partial to create a partial function for making requests - make_request = functools.partial(self._sess.request, formatted_path, method) + make_request = functools.partial( + self.session.request, formatted_path, method + ) # Store the partial function on the instance setattr(self, f"_{func.__name__}_request", make_request) @@ -113,9 +147,35 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any: return decorator -class HGraphRouter(ABC): +class RouterMixin: + + def _invoke_request_registered( + self, placeholders: dict = None, validator=ResponseValidation(), **kwargs: Any + ): + """ + Make an HTTP request using the stored partial request function. + Args: + **kwargs (Any): Keyword arguments to be passed to the request function. + Returns: + Any: The response from the HTTP request. + """ + frame = inspect.currentframe().f_back + fname = frame.f_code.co_name + route = RouterRegistry().routers.get(f"{self.__class__.__name__}.{fname}") + + if re.search(r"{\w+}", route.path): + assert placeholders is not None, "Placeholders must be provided" + formatted_path = route.path.format(**placeholders) + else: + formatted_path = route.path + + log.debug( # pylint: disable=logging-fstring-interpolation + f"Invoke request registered with router: {route.method}: " + f"{self.__class__.__name__}.{fname}: {formatted_path}" + ) + return route.request_func(formatted_path, validator=validator, **kwargs) - def _invoke_request(self, **kwargs: Any): + def _invoke_request(self, validator=ResponseValidation(), **kwargs: Any): """ Make an HTTP request using the stored partial request function. @@ -127,4 +187,7 @@ def _invoke_request(self, **kwargs: Any): """ frame = inspect.currentframe().f_back fname = frame.f_code.co_name - return getattr(self, f"_{fname}_request")(**kwargs) + log.debug( # pylint: disable=logging-fstring-interpolation + f"Invoke request: {str(self.__class__.__name__)}.{fname}" + ) + return getattr(self, f"_{fname}_request")(validator=validator, **kwargs) diff --git a/hugegraph-python-client/src/pyhugegraph/utils/log.py b/hugegraph-python-client/src/pyhugegraph/utils/log.py index 18dac658..2f5153fd 100755 --- a/hugegraph-python-client/src/pyhugegraph/utils/log.py +++ b/hugegraph-python-client/src/pyhugegraph/utils/log.py @@ -31,10 +31,12 @@ def init_log(log_file="logs/output.log"): # Create a logger log = logging.getLogger(__name__) - log.setLevel(logging.DEBUG) + log.setLevel(logging.INFO) # Create a handler for writing to log file - file_handler = TimedRotatingFileHandler(log_file, when='midnight', interval=1, backupCount=3, encoding='utf-8') + file_handler = TimedRotatingFileHandler( + log_file, when="midnight", interval=1, backupCount=3, encoding="utf-8" + ) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT)) log.addHandler(file_handler) @@ -46,7 +48,7 @@ class CustomConsoleHandler(logging.StreamHandler): "INFO": "\033[0;32m", # Green "WARNING": "\033[0;33m", # Yellow "ERROR": "\033[0;31m", # Red - "CRITICAL": "\033[0;41m" # Red background + "CRITICAL": "\033[0;41m", # Red background } def emit(self, record): diff --git a/hugegraph-python-client/src/pyhugegraph/utils/util.py b/hugegraph-python-client/src/pyhugegraph/utils/util.py index 5ca36b33..256b672b 100644 --- a/hugegraph-python-client/src/pyhugegraph/utils/util.py +++ b/hugegraph-python-client/src/pyhugegraph/utils/util.py @@ -16,12 +16,15 @@ # under the License. import json +import traceback +import requests from pyhugegraph.utils.exceptions import ( ServiceUnavailableException, NotAuthorizedError, NotFoundError, ) +from pyhugegraph.utils.log import log def create_exception(response_content): @@ -60,3 +63,68 @@ def check_if_success(response, error=None): ) raise error return True + + +class ResponseValidation: + def __init__(self, content_type: str = "json", strict: bool = True) -> None: + super().__init__() + self._content_type = content_type + self._strict = strict + + def __call__(self, response: requests.Response, method: str, path: str): + """ + Validate the HTTP response according to the provided content type and strictness. + + :param response: HTTP response object + :param method: HTTP method used (e.g., 'GET', 'POST') + :param path: URL path of the request + :return: Parsed response content or empty dict if none applicable + """ + result = {} + + try: + response.raise_for_status() + + if response.status_code == 204: + log.debug("No content returned (204) for %s: %s", method, path) + else: + if self._content_type == "raw": + result = response + elif self._content_type == "json": + result = response.json() + elif self._content_type == "text": + result = response.text + else: + raise ValueError(f"Unknown content type: {self._content_type}") + + except requests.exceptions.HTTPError as e: + if not self._strict and response.status_code == 404: + log.info( # pylint: disable=logging-fstring-interpolation + f"Resource {path} not found (404)" + ) + else: + try: + details = response.json().get( + "exception", "key 'exception' not found" + ) + except (ValueError, KeyError): + details = "key 'exception' not found" + + log.error( # pylint: disable=logging-fstring-interpolation + f"{method}: {e}; Server Exception: {details}" + ) + + if response.status_code == 404: + raise NotFoundError(response.content) from e + + raise e + + except Exception: # pylint: disable=broad-exception-caught + log.error( # pylint: disable=logging-fstring-interpolation + f"Unhandled exception occurred: {traceback.format_exc()}" + ) + + return result + + def __repr__(self) -> str: + return f"ResponseValidation(content_type={self._content_type}, strict={self._strict})" diff --git a/style/pylint.conf b/style/pylint.conf index 80ebc616..ed699829 100644 --- a/style/pylint.conf +++ b/style/pylint.conf @@ -447,7 +447,8 @@ disable=raw-checker-failed, R0902, # Too many instance attributes (11/7) R1725, # Consider using Python 3 style super() without arguments (super-with-arguments) W0622, # Redefining built-in 'id' (redefined-builtin) - R0904 # Too many public methods (27/20) (too-many-public-methods) + R0904, # Too many public methods (27/20) (too-many-public-methods) + E1120 # TODO: unbound-method-call-no-value-for-parameter # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option