diff --git a/demos/demos_databases_apis/spanner/google_spanner_finance_graph.ipynb b/demos/demos_databases_apis/spanner/google_spanner_finance_graph.ipynb index b19a2cee6..3039268ba 100644 --- a/demos/demos_databases_apis/spanner/google_spanner_finance_graph.ipynb +++ b/demos/demos_databases_apis/spanner/google_spanner_finance_graph.ipynb @@ -95,31 +95,50 @@ "!gcloud auth application-default login" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "11e41457-303c-4d5e-ae0e-7015db33d9f7", + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "markdown", - "id": "56bc01a7-76ea-44f6-b1cc-c5488c5c5922", + "id": "88eb24b0-7d3b-4629-813c-fc989ba1ea90", "metadata": {}, "source": [ - "### Spanner GQL Query to Graphistry Visualization" + "### Example 1: GQL Path Query to Graphistry Visualization of all nodes and edges (LIMIT optional) \n", + "\n", + "to extract the data from Spanner Graph as a graph with nodes and edges in a single object, a GQL path query is required. \n", + "\n", + "The format of a path query is as follows, note the p= at the start of the MATCH clause, and the SAFE_TO_JSON(p) without these, \n", + "the query will not produce the results needed to properly load a graphistry graph. LIMIT is optional, but for large graphs with millions\n", + " of edges or more, it's best to filter either in the query or use LIMIT so as not to exhaust GPU memory. \n", + "\n", + "```python\n", + "GRAPH FinGraph\n", + "MATCH p = (a)-[b]->(c) where 1=1 LIMIT 10000 return SAFE_TO_JSON(p) as path\n", + "```\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "e7ab1379-14bf-4c03-b5b2-28df22609029", + "id": "58ee08b2-29e1-47db-b0a8-440f6171e54d", "metadata": {}, "outputs": [], "source": [ "query=f'''GRAPH FinGraph\n", - "MATCH p = (a)-[b]->(c) where 1=1 {LIMIT_CLAUSE} return TO_JSON(p) as path'''\n", + "MATCH p = (a)-[b]->(c) where 1=1 {LIMIT_CLAUSE} return SAFE_TO_JSON(p) as path'''\n", "\n", - "g = graphistry.spanner_query(query)" + "g = graphistry.spanner_gql_to_g(query)" ] }, { "cell_type": "code", "execution_count": null, - "id": "aeead725-928a-44fe-b5b5-630c830502e0", + "id": "3d606a3f-e807-4fa7-893e-52a95d238cc0", "metadata": {}, "outputs": [], "source": [ @@ -129,23 +148,23 @@ { "cell_type": "code", "execution_count": null, - "id": "5f172d3e-108a-4a18-a88e-e012988013c5", + "id": "ae275af9-f354-454b-bbeb-e423ae4acfba", "metadata": {}, "outputs": [], "source": [] }, { "cell_type": "markdown", - "id": "b40b6dae-0770-4839-a392-7cf16bee65d6", + "id": "b9d18502-9bc1-4f7e-af3a-b99f7d848e08", "metadata": {}, "source": [ - "#### inspect contents of graphistry graph (nodes and edges): " + "#### Example 1.1 - inspect contents of graphistry graph (nodes and edges): " ] }, { "cell_type": "code", "execution_count": null, - "id": "1fed46dd-2bb5-4563-9a21-920d299ba30d", + "id": "42e451d7-8f97-45a8-bb45-7c4271f11f68", "metadata": {}, "outputs": [], "source": [ @@ -155,7 +174,7 @@ { "cell_type": "code", "execution_count": null, - "id": "ad33fde2-3aae-4da0-8532-41d489c6fff1", + "id": "e5078632-24b2-4139-8e4f-a2c18f1efd94", "metadata": {}, "outputs": [], "source": [ @@ -165,7 +184,7 @@ { "cell_type": "code", "execution_count": null, - "id": "279f4cb9-daa1-4c2b-af69-85a5b0a771fb", + "id": "376bf2a7-931a-4c3f-bfda-3413734e5ad7", "metadata": {}, "outputs": [], "source": [ @@ -175,15 +194,67 @@ { "cell_type": "code", "execution_count": null, - "id": "2a60ee00-055d-4cee-b286-c5e3d6d85f09", + "id": "05d8b293-62b7-4056-b924-ff04284559b9", "metadata": {}, "outputs": [], "source": [] }, + { + "cell_type": "markdown", + "id": "0013cceb-f32f-48a0-9f02-6b75a2704294", + "metadata": {}, + "source": [ + "### Example 2: Spanner GQL Query to pandas dataframe (LIMIT optional) \n", + "\n", + "This example shows a non-path query that returns tabular results, which are then convered to a dataframe for easy manipulation and inspection of the results. \n", + "\n", + "```python\n", + "GRAPH FinGraph \n", + "MATCH (p:Person)-[]-()->(l:Loan)\n", + "RETURN p.id as ID, p.name AS Name, SUM(l.loan_amount) AS TotalBorrowed\n", + "ORDER BY TotalBorrowed DESC\n", + "LIMIT 10```\n", + "\n" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "11e41457-303c-4d5e-ae0e-7015db33d9f7", + "id": "523f57b2-d09f-4aa2-8626-5af74d5d9a20", + "metadata": {}, + "outputs": [], + "source": [ + "query_top10='''GRAPH FinGraph \n", + "MATCH (p:Person)-[]-()->(l:Loan) WHERE 1=1\n", + "RETURN p.id as ID, p.name AS Name, SUM(l.loan_amount) AS TotalBorrowed\n", + "ORDER BY TotalBorrowed DESC\n", + "LIMIT 10'''" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a51917d3-6c17-4438-981d-b7d441ec89ec", + "metadata": {}, + "outputs": [], + "source": [ + "Top10_Borrowers_df = graphistry.spanner_query_to_df(query_top10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aab6fb57-6f09-41ae-927a-4f7b5f47db19", + "metadata": {}, + "outputs": [], + "source": [ + "Top10_Borrowers_df.head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a55789a-16ed-4b6f-b2d0-e374999f1806", "metadata": {}, "outputs": [], "source": [] diff --git a/graphistry/PlotterBase.py b/graphistry/PlotterBase.py index 48b0a7739..ddc1ac5b0 100644 --- a/graphistry/PlotterBase.py +++ b/graphistry/PlotterBase.py @@ -2273,22 +2273,38 @@ def bolt(self, driver): res._bolt_driver = to_bolt_driver(driver) return res - # TODO(tcook): add pydocs, typing - def spanner_init(self, spanner_config): + def spanner_init(self: Plottable, spanner_config: Dict[str, str]) -> Plottable: + """ + Initializes a SpannerGraph object with the provided configuration and connects to the instance db + + spanner_config dict must contain the include the following keys, credentials_file is optional: + - "project_id": The GCP project ID. + - "instance_id": The Spanner instance ID. + - "database_id": The Spanner database ID. + - "credentials_file": json file API key for service accounts + + :param spanner_config A dictionary containing the Spanner configuration. + :type (Dict[str, str]) + :return: Plottable with a Spanner connection + :rtype: Plottable + :raises ValueError: If any of the required keys in `spanner_config` are missing or have invalid values. + + """ res = copy.copy(self) project_id = spanner_config["project_id"] instance_id = spanner_config["instance_id"] database_id = spanner_config["database_id"] + credentials_file = spanner_config["credentials_file"] # check if valid required_keys = ["project_id", "instance_id", "database_id"] for key in required_keys: value = spanner_config.get(key) - if not value: # checks for None or empty values + if not value: # check for None or empty values raise ValueError(f"Missing or invalid value for required Spanner configuration: '{key}'") - res._spannergraph = SpannerGraph(res, project_id, instance_id, database_id) + res._spannergraph = SpannerGraph(res, project_id, instance_id, database_id, credentials_file) logger.debug("Created SpannerGraph object: {res._spannergraph}") return res @@ -2481,28 +2497,91 @@ def cypher(self, query: str, params: Dict[str, Any] = {}) -> Plottable: .edges(edges) - def spanner_query(self, query: str, params: Dict[str, Any] = {}) -> Plottable: + def spanner_gql_to_g(self: Plottable, query: str) -> Plottable: """ - TODO(tcook): maybe rename to spanner_query_gql since spanner supports multiple languages. SQL, GQL, etc + Submit GQL query to google spanner graph database and return Plottable with nodes and edges populated + + GQL must be a path query with a syntax similar to the following, it's recommended to return the path with + SAFE_TO_JSON(p), TO_JSON() can also be used, but not recommend. LIMIT is optional, but for large graphs with millions + of edges or more, it's best to filter either in the query or use LIMIT so as not to exhaust GPU memory. + + query=f'''GRAPH my_graph + MATCH p = (a)-[b]->(c) LIMIT 100000 return SAFE_TO_JSON(p) as path''' - query google spanner graph database and return Plottable with nodes and edges populated :param query: GQL query string :type query: Str + :returns: Plottable with the results of GQL query as a graph :rtype: Plottable - **Example: calling spanner_query + **Example: calling spanner_gql_to_g + :: + + import graphistry + + # credentials_file is optional, all others are required + SPANNER_CONF = { "project_id": PROJECT_ID, + "instance_id": INSTANCE_ID, + "database_id": DATABASE_ID, + "credentials_file": CREDENTIALS_FILE } + + graphistry.register(..., spanner_config=SPANNER_CONF) + + query=f'''GRAPH my_graph + MATCH p = (a)-[b]->(c) LIMIT 100000 return SAFE_TO_JSON(p) as path''' + + g = graphistry.spanner_gql_to_g(query) + + g.plot() + + """ + + from .pygraphistry import PyGraphistry + + res = copy.copy(self) + + if res._spannergraph is None: + spanner_config = PyGraphistry._config["spanner"] + if spanner_config is not None: + logger.debug(f"Spanner Config: {spanner_config}") + else: + logger.warn(f'PyGraphistry._config["spanner"] is None') + + res = res.spanner_init(PyGraphistry._config["spanner"]) + + return res._spannergraph.gql_to_graph(query) + + def spanner_query_to_df(self: Plottable, query: str) -> pd.DataFrame: + """ + + Submit query to google spanner database and return a df of the results + + query can be SQL or GQL as long as table of results are returned + + query='SELECT * from Account limit 10000' + + :param query: query string + :type query: Str + + :returns: Pandas DataFrame with the results of query + :rtype: pd.DataFrame + + **Example: calling spanner_query_to_df :: import graphistry - SPANNER_CONF = { "project_id": PROJECT_ID, + # credentials_file is optional, all others are required + SPANNER_CONF = { "project_id": PROJECT_ID, "instance_id": INSTANCE_ID, - "database_id": DATABASE_ID } + "database_id": DATABASE_ID, + "credentials_file": CREDENTIALS_FILE } graphistry.register(..., spanner_config=SPANNER_CONF) - g = graphistry.spanner_query("Graph MyGraph\nMATCH ()-[]->()" ) + query='SELECT * from Account limit 10000' + + df = graphistry.spanner_query_to_df(query) g.plot() @@ -2517,12 +2596,11 @@ def spanner_query(self, query: str, params: Dict[str, Any] = {}) -> Plottable: if spanner_config is not None: logger.debug(f"Spanner Config: {spanner_config}") else: - logger.debug(f'PyGraphistry._config["spanner"] is None') + logger.warn(f'PyGraphistry._config["spanner"] is None') res = res.spanner_init(PyGraphistry._config["spanner"]) - return res._spannergraph.gql_to_graph(query) - else: - return res._spannergraph.gql_to_graph(query) + + return res._spannergraph.query_to_df(query) def nodexl(self, xls_or_url, source='default', engine=None, verbose=False): diff --git a/graphistry/__init__.py b/graphistry/__init__.py index 5ff53900b..04e255de8 100644 --- a/graphistry/__init__.py +++ b/graphistry/__init__.py @@ -31,7 +31,8 @@ bolt, cypher, tigergraph, - spanner_query, + spanner_gql_to_g, + spanner_query_to_df, gsql, gsql_endpoint, cosmos, diff --git a/graphistry/plugins/spannergraph.py b/graphistry/plugins/spannergraph.py index 01d096de0..51323ad7e 100644 --- a/graphistry/plugins/spannergraph.py +++ b/graphistry/plugins/spannergraph.py @@ -4,13 +4,11 @@ import time from typing import Any, List, Dict +from graphistry.Plottable import Plottable + from graphistry.util import setup_logger logger = setup_logger(__name__) -import logging -logging.basicConfig(level=logging.INFO) - -from google.cloud.spanner_v1.data_types import JsonObject class SpannerConnectionError(Exception): """Custom exception for errors related to Spanner connection.""" @@ -21,31 +19,19 @@ class SpannerQueryResult: Encapsulates the results of a query, including metadata. :ivar list data: The raw query results. - :ivar float execution_time: The time taken to execute the query. - :ivar int record_count: The number of records returned. """ - def __init__(self, data: List[Any], execution_time: float): + def __init__(self, data: List[Any], column_names: List[str]=None): """ Initializes a SpannerQueryResult instance. :param data: The raw query results. - :param execution_time: The time taken to execute the query. + :type List[Any] + :param column_names: a list of the column names from the cursor, defaults to None + :type: List[str], optional """ self.data = data - self.execution_time = execution_time - self.record_count = len(data) - - def summary(self) -> Dict[str, Any]: - """ - Provides a summary of the query execution. - - :return: A summary of the query results. - """ - return { - "execution_time": self.execution_time, - "record_count": self.record_count - } + self.column_names = column_names class SpannerGraph: @@ -59,7 +45,7 @@ class SpannerGraph: :ivar Any graphistry: The Graphistry parent object. """ - def __init__(self, graphistry: Any, project_id: str, instance_id: str, database_id: str): + def __init__(self, g: Plottable, project_id: str, instance_id: str, database_id: str, credentials_file: str=None): """ Initializes the SpannerGraph instance. @@ -68,10 +54,11 @@ def __init__(self, graphistry: Any, project_id: str, instance_id: str, database_ :param instance_id: The Spanner instance ID. :param database_id: The Spanner database ID. """ - self.graphistry = graphistry + self.g = g self.project_id = project_id self.instance_id = instance_id self.database_id = database_id + self.credentials_file = credentials_file self.connection = self.__connect() def __connect(self) -> Any: @@ -85,9 +72,13 @@ def __connect(self) -> Any: from google.cloud.spanner_dbapi.connection import connect try: - connection = connect(self.instance_id, self.database_id) + if self.credentials_file: + connection = connect(self.instance_id, self.database_id, credentials=self.credentials_file) + else: + connection = connect(self.instance_id, self.database_id) + connection.autocommit = True - logging.info("Connected to Spanner database.") + logger.info("Connected to Spanner database.") return connection except Exception as e: raise SpannerConnectionError(f"Failed to connect to Spanner: {e}") @@ -98,13 +89,14 @@ def close_connection(self) -> None: """ if self.connection: self.connection.close() - logging.info("Connection to Spanner database closed.") + logger.info("Connection to Spanner database closed.") def execute_query(self, query: str) -> SpannerQueryResult: """ Executes a GQL query on the Spanner database. - :param query: The GQL query to execute. + :param query: The GQL query to execute + :type str :return: The results of the query execution. :rtype: SpannerQueryResult :raises RuntimeError: If the query execution fails. @@ -116,14 +108,16 @@ def execute_query(self, query: str) -> SpannerQueryResult: cursor = self.connection.cursor() cursor.execute(query) results = cursor.fetchall() - execution_time = time.time() - start_time - logging.info(f"Query executed in {execution_time:.4f} seconds.") - return SpannerQueryResult(results, execution_time) + column_names = [desc[0] for desc in cursor.description] # extract column names + logger.debug(f'column names returned from query: {column_names}') + execution_time_s = time.time() - start_time + logger.info(f"Query completed in {execution_time_s:.3f} seconds.") + return SpannerQueryResult(results, column_names) except Exception as e: raise RuntimeError(f"Query execution failed: {e}") @staticmethod - def convert_spanner_json(data): + def convert_spanner_json(data: List[Any]) -> List[Dict[str, Any]]: from google.cloud.spanner_v1.data_types import JsonObject json_list = [] for item in data: @@ -156,6 +150,35 @@ def convert_spanner_json(data): json_list.append(json_entry) return json_list + @staticmethod + def add_type_from_label_to_df(df: pd.DataFrame) -> pd.DataFrame: + """ + Modify input DataFrame creating a 'type' column is created from 'label' for proper type handling in Graphistry + If a 'type' column already exists, it is renamed to 'type_' before creating the new 'type' column. + + :param df: DataFrame containing node or edge data + :type df: pd.DataFrame + + :return: Modified DataFrame with the updated 'type' column. + :rtype: query: pd.DataFrame + + """ + + # rename 'type' to 'type_' if it exists + if "type" in df.columns: + df.rename(columns={"type": "type_"}, inplace=True) + logger.info("'type' column renamed to 'type_'") + + # check if 'label' column exists before assigning it to 'type' + if "label" in df.columns: + df["type"] = df["label"] + else: + # assign None value if 'label' is missing + df["type"] = None + logger.warn("'label' column missing, 'type' set to None") + + return df + @staticmethod def get_nodes_df(json_data: list) -> pd.DataFrame: """ @@ -176,26 +199,20 @@ def get_nodes_df(json_data: list) -> pd.DataFrame: ] nodes_df = pd.DataFrame(nodes).drop_duplicates() - # if 'type' property exists, skip setting and warn - if "type" not in nodes_df.columns: - # check 'label' column exists before assigning it to 'type' - if "label" in nodes_df.columns: - nodes_df['type'] = nodes_df['label'] - else: - nodes_df['type'] = None # Assign a default value if 'label' is missing - else: - logger.warn("unable to assign 'type' from label, column exists\n") - - return nodes_df + return SpannerGraph.add_type_from_label_to_df(nodes_df) + + @staticmethod def get_edges_df(json_data: list) -> pd.DataFrame: """ Converts spanner json edges into a pandas DataFrame :param json_data: The structured JSON data containing graph edges. + :type list :return: A DataFrame containing edge information. :rtype: pd.DataFrame + """ edges = [ { @@ -210,85 +227,41 @@ def get_edges_df(json_data: list) -> pd.DataFrame: ] edges_df = pd.DataFrame(edges).drop_duplicates() - # if 'type' property exists, skip setting and warn - if "type" not in edges_df.columns: - # check 'label' column exists before assigning it to 'type' - if "label" in edges_df.columns: - edges_df['type'] = edges_df['label'] - else: - edges_df['type'] = None # Assign a default value if 'label' is missing - else: - logger.warn("unable to assign 'type' from label, column exists\n") + return SpannerGraph.add_type_from_label_to_df(edges_df) - return edges_df - def gql_to_graph(self, query: str) -> Any: + def gql_to_graph(self, query: str) -> Plottable: """ Executes a query and constructs a Graphistry graph from the results. :param query: The GQL query to execute. :return: A Graphistry graph object constructed from the query results. + :rtype: Plottable """ query_result = self.execute_query(query) + # convert json result set to a list query_result_list = [ query_result.data ] + json_data = self.convert_spanner_json(query_result_list) + nodes_df = self.get_nodes_df(json_data) edges_df = self.get_edges_df(json_data) - # TODO(tcook): add more error handling here if nodes or edges are empty - g = self.graphistry.nodes(nodes_df, 'identifier').edges(edges_df, 'source', 'destination') - return g - # TODO(tcook): add wrapper funcs in PlotterBase for these utility functions: - - def get_schema(self) -> Dict[str, List[Dict[str, str]]]: - """ - Retrieves the schema of the Spanner database. + # TODO(tcook): add more error handling here if nodes or edges are empty + return self.g.nodes(nodes_df, 'identifier').edges(edges_df, 'source', 'destination') - :return: A dictionary containing table names and column details. + def query_to_df(self, query: str) -> pd.DataFrame: """ - schema = {} - try: - cursor = self.connection.cursor() - cursor.execute("SELECT table_name, column_name, spanner_type FROM information_schema.columns") - for row in cursor.fetchall(): - table_name, column_name, spanner_type = row - if table_name not in schema: - schema[table_name] = [] - schema[table_name].append({"column_name": column_name, "type": spanner_type}) - logging.info("Database schema retrieved successfully.") - except Exception as e: - logging.error(f"Failed to retrieve schema: {e}") - return schema + Executes a query and returns a pandas dataframe of results - def validate_data(self, data: Dict[str, List[Dict[str, Any]]], schema: Dict[str, List[Dict[str, str]]]) -> bool: + :param query: The query to execute. + :return: pandas dataframe of the query results + :rtype: pd.DataFrame """ - Validates input data against the database schema. + query_result = self.execute_query(query) - :param data: The data to validate. - :param schema: The schema of the database. - :return: True if the data is valid, False otherwise. - """ - for table, columns in data.items(): - if table not in schema: - logging.error(f"Table {table} does not exist in schema.") - return False - for record in columns: - for key in record.keys(): - if key not in [col["column_name"] for col in schema[table]]: - logging.error(f"Column {key} is not valid for table {table}.") - return False - logging.info("Data validation passed.") - return True - - def dump_config(self) -> Dict[str, str]: - """ - Returns the current configuration of the SpannerGraph instance. + # create DataFrame from json results, adding column names + df = pd.DataFrame(query_result.data, columns=query_result.column_names) - :return: A dictionary containing configuration details. - """ - return { - "project_id": self.project_id, - "instance_id": self.instance_id, - "database_id": self.database_id - } + return df diff --git a/graphistry/pygraphistry.py b/graphistry/pygraphistry.py index 50a320a51..7c1ef9066 100644 --- a/graphistry/pygraphistry.py +++ b/graphistry/pygraphistry.py @@ -580,7 +580,7 @@ def set_spanner_config(spanner_config): :returns: None. :rtype: None - **Example: calling set_spanner_config** + **Example: calling set_spanner_config - all keys are required** :: import graphistry @@ -591,7 +591,20 @@ def set_spanner_config(spanner_config): "database_id": DATABASE_ID } graphistry.set_spanner_config(SPANNER_CONF) - + + **Example: calling set_spanner_config with credentials_file (optional) - used for service accounts** + :: + + import graphistry + graphistry.register(...) + + SPANNER_CONF = { "project_id": PROJECT_ID, + "instance_id": INSTANCE_ID, + "database_id": DATABASE_ID, + "credentials_file": CREDENTIALS_FILE } + + graphistry.set_spanner_config(SPANNER_CONF) + """ if spanner_config is not None: @@ -1883,10 +1896,83 @@ def tigergraph( ) @staticmethod - def spanner_query(query: str, params: Dict[str, Any] = {}) -> Plottable: - # TODO(tcook): add pydocs - return Plotter().spanner_query(query, params) + def spanner_gql_to_g(query: str) -> Plottable: + """ + Submit GQL query to google spanner graph database and return Plottable with nodes and edges populated + + GQL must be a path query with a syntax similar to the following, it's recommended to return the path with + SAFE_TO_JSON(p), TO_JSON() can also be used, but not recommend. LIMIT is optional, but for large graphs with millions + of edges or more, it's best to filter either in the query or use LIMIT so as not to exhaust GPU memory. + + query=f'''GRAPH my_graph + MATCH p = (a)-[b]->(c) LIMIT 100000 return SAFE_TO_JSON(p) as path''' + + :param query: GQL query string + :type query: Str + + :returns: Plottable with the results of GQL query as a graph + :rtype: Plottable + + **Example: calling spanner_gql_to_g + :: + + import graphistry + + # credentials_file is optional, all others are required + SPANNER_CONF = { "project_id": PROJECT_ID, + "instance_id": INSTANCE_ID, + "database_id": DATABASE_ID, + "credentials_file": CREDENTIALS_FILE } + + graphistry.register(..., spanner_config=SPANNER_CONF) + + query=f'''GRAPH my_graph + MATCH p = (a)-[b]->(c) LIMIT 100000 return SAFE_TO_JSON(p) as path''' + + g = graphistry.spanner_gql_to_g(query) + g.plot() + + """ + return Plotter().spanner_gql_to_g(query) + + @staticmethod + def spanner_query_to_df(query: str) -> pd.DataFrame: + """ + + Submit query to google spanner database and return a df of the results + + query can be SQL or GQL as long as table of results are returned + + query='SELECT * from Account limit 10000' + + :param query: query string + :type query: Str + + :returns: Pandas DataFrame with the results of query + :rtype: pd.DataFrame + + **Example: calling spanner_query_to_df + :: + + import graphistry + + # credentials_file is optional, all others are required + SPANNER_CONF = { "project_id": PROJECT_ID, + "instance_id": INSTANCE_ID, + "database_id": DATABASE_ID, + "credentials_file": CREDENTIALS_FILE } + + graphistry.register(..., spanner_config=SPANNER_CONF) + + query='SELECT * from Account limit 10000' + + df = graphistry.spanner_query_to_df(query) + + g.plot() + + """ + return Plotter().spanner_query_to_df(query) @staticmethod def gsql_endpoint( @@ -2549,7 +2635,8 @@ def _handle_api_response(response): cypher = PyGraphistry.cypher nodexl = PyGraphistry.nodexl tigergraph = PyGraphistry.tigergraph -spanner_query = PyGraphistry.spanner_query +spanner_gql_to_g = PyGraphistry.spanner_gql_to_g +spanner_query_to_df = PyGraphistry.spanner_query_to_df cosmos = PyGraphistry.cosmos neptune = PyGraphistry.neptune gremlin = PyGraphistry.gremlin