From 215f478ef59db7b21047b4760e3394a032e5007c Mon Sep 17 00:00:00 2001 From: Sam Claassens Date: Thu, 30 Mar 2023 14:01:21 -0500 Subject: [PATCH 1/8] Adding the SDKCommonGQL submodule --- .gitmodules | 3 +++ sdkCommonGQL | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 sdkCommonGQL diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ba64c6b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "sdkCommonGQL"] + path = sdkCommonGQL + url = git@github.com:NavAbility/SDKCommonGQL.git diff --git a/sdkCommonGQL b/sdkCommonGQL new file mode 160000 index 0000000..8589954 --- /dev/null +++ b/sdkCommonGQL @@ -0,0 +1 @@ +Subproject commit 85899542f4a23f5cfbe5f62fe0f01834f0a1ddb7 From 18c54daa39a2de2623771aaab08a5650ed3033b8 Mon Sep 17 00:00:00 2001 From: Danial Gawryjolek Date: Wed, 5 Apr 2023 02:33:37 +0200 Subject: [PATCH 2/8] add toml --- .vscode/settings.json | 5 +++++ sdkCommonGQL | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..af7446d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.analysis.extraPaths": [ + "./src" + ] +} \ No newline at end of file diff --git a/sdkCommonGQL b/sdkCommonGQL index 8589954..b8f4edb 160000 --- a/sdkCommonGQL +++ b/sdkCommonGQL @@ -1 +1 @@ -Subproject commit 85899542f4a23f5cfbe5f62fe0f01834f0a1ddb7 +Subproject commit b8f4edb85bb4aa0c2b2bc04e1492e5bebf7bbfcf From cd3d604e7a6651e299ecfb595376d69565430168 Mon Sep 17 00:00:00 2001 From: Danial Gawryjolek Date: Wed, 5 Apr 2023 02:47:50 +0200 Subject: [PATCH 3/8] add loader --- src/navability/services/loader.py | 81 +++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/navability/services/loader.py diff --git a/src/navability/services/loader.py b/src/navability/services/loader.py new file mode 100644 index 0000000..dd8d80b --- /dev/null +++ b/src/navability/services/loader.py @@ -0,0 +1,81 @@ +import os +import toml +import re + +class FragmentData: + def __init__(self, name: str, data: str): + self.name = name + self.data = data + + def __str__(self) -> str: + return f"{self.name}\n{self.data}" + +class Fragment: + def __init__(self, name: str, data: list[FragmentData]): + self.name = name + self.data = data + + def __str__(self) -> str: + return f"{self.name}:\n" + "\n".join([str(d) for d in self.data]) + +class Query: + def __init__(self, name: str, data: str): + self.name = name + self.data = data + + def __str__(self) -> str: + return f"{self.name}:\n{self.data}" + +def get_files(folder_path: str, extension: str) -> list[str]: + files = [] + for file in os.listdir(folder_path): + file_path = os.path.join(folder_path, file) + if os.path.isdir(file_path): + files += get_files(file_path, extension) + elif file.endswith(extension): + files.append(file_path) + return files + +def get_fragment_name(fragment_string: str) -> str: + pattern = r"fragment\s+(\S+)\s+on" + match = re.search(pattern, fragment_string) + if match: + return match.group(1) + else: + return None + +def get_queries(folder_path: str) -> list[Query]: + files = get_files(folder_path, ".toml") + + fragments = [] + queries = [] + + for file in files: + with open(file, "r") as f: + data = toml.load(f) + + fragment_data_map = {} + + for fragment_data in data["fragment"]: + name = fragment_data["name"] + fragmentData_objects = [FragmentData(get_fragment_name(d["data"]), d["data"]) for d in fragment_data["data"]] + fragment = Fragment(name = name, data = fragmentData_objects) + fragments.append(fragment) + for fd in fragmentData_objects: + if fd.name not in fragment_data_map: + fragment_data_map[fd.name] = [] + fragment_data_map[fd.name].append(fd) + + + for query_data in data["queries"]: + name = query_data["name"] + query = Query(name = name, data = query_data["data"]) + for fd_name, fd_list in fragment_data_map.items(): + if fd_name in query_data["data"]: + for fd in fd_list: + query.data += "\n" + "\n" + fd.data + queries.append(query) + + return queries + +__all__ = ['get_queries'] \ No newline at end of file From 5fae8c59e71d05a08c22656a3a391573182d4537 Mon Sep 17 00:00:00 2001 From: Danial Gawryjolek Date: Wed, 5 Apr 2023 22:25:08 +0200 Subject: [PATCH 4/8] add dict for query Co-authored-by: Sam Claassens --- src/navability/services/loader.py | 16 ++++++++++++---- src/navability/services/variable.py | 23 ++++++++++++----------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/navability/services/loader.py b/src/navability/services/loader.py index dd8d80b..a4d16e9 100644 --- a/src/navability/services/loader.py +++ b/src/navability/services/loader.py @@ -2,6 +2,8 @@ import toml import re +from gql import gql + class FragmentData: def __init__(self, name: str, data: str): self.name = name @@ -44,11 +46,11 @@ def get_fragment_name(fragment_string: str) -> str: else: return None -def get_queries(folder_path: str) -> list[Query]: +def get_queries(folder_path: str) -> dict[str, Query]: files = get_files(folder_path, ".toml") fragments = [] - queries = [] + queries = {} for file in files: with open(file, "r") as f: @@ -74,8 +76,14 @@ def get_queries(folder_path: str) -> list[Query]: if fd_name in query_data["data"]: for fd in fd_list: query.data += "\n" + "\n" + fd.data - queries.append(query) + # GQL interpret the query/mutation + query.data = gql(query.data) + # Add to dictionary + queries[name] = query return queries -__all__ = ['get_queries'] \ No newline at end of file +__all__ = ['get_queries'] + +# Load and export +GQL_QUERIES = get_queries(os.path.join(".", "sdkCommonGQL")) \ No newline at end of file diff --git a/src/navability/services/variable.py b/src/navability/services/variable.py index 7558326..72b5db0 100644 --- a/src/navability/services/variable.py +++ b/src/navability/services/variable.py @@ -3,13 +3,14 @@ from gql import gql -from navability.common.mutations import GQL_ADDVARIABLE -from navability.common.queries import ( - GQL_FRAGMENT_VARIABLES, - GQL_LISTVARIABLES, - GQL_GETVARIABLE, - GQL_GETVARIABLES, -) +# from navability.common.mutations import GQL_ADDVARIABLE +from navability.services.loader import GQL_QUERIES +# from navability.common.queries import ( +# GQL_FRAGMENT_VARIABLES, +# GQL_LISTVARIABLES, +# GQL_GETVARIABLE, +# GQL_GETVARIABLES, +# ) from navability.entities.client import Client from navability.entities.navabilityclient import ( MutationOptions, @@ -39,7 +40,7 @@ async def _addVariable(navAbilityClient: NavAbilityClient, client: Client, v: Variable): result = await navAbilityClient.mutate( MutationOptions( - gql(GQL_ADDVARIABLE), + GQL_QUERIES["GQL_ADDVARIABLE"].data, {"variable": {"client": client.dump(), "packedData": v.dumpsPacked()}}, ) ) @@ -104,7 +105,7 @@ async def listVariables( } logger.debug(f"Query params: {params}") res = await client.query( - QueryOptions(gql(GQL_LISTVARIABLES), params) + QueryOptions(GQL_QUERIES["GQL_LISTVARIABLES"].data, params) ) if ( "users" not in res @@ -177,7 +178,7 @@ async def getVariables( } logger.debug(f"Query params: {params}") res = await client.query( - QueryOptions(gql(GQL_FRAGMENT_VARIABLES + GQL_GETVARIABLES), params) + QueryOptions(GQL_QUERIES["GQL_FRAGMENT_VARIABLES"].data+ GQL_GETVARIABLES), params) ) logger.debug(f"Query result: {res}") # TODO: Check for errors @@ -215,7 +216,7 @@ async def getVariable( params["label"] = label logger.debug(f"Query params: {params}") res = await client.query( - QueryOptions(gql(GQL_FRAGMENT_VARIABLES + GQL_GETVARIABLE), params) + QueryOptions(GQL_QUERIES["GQL_FRAGMENT_VARIABLES"].data+ GQL_GETVARIABLE), params) ) logger.debug(f"Query result: {res}") # TODO: Check for errors From beabc3489e738ad8d57eebbe26f54e0dc6b26912 Mon Sep 17 00:00:00 2001 From: Danial Gawryjolek Date: Fri, 7 Apr 2023 00:43:34 +0200 Subject: [PATCH 5/8] update loader add comments add suggestion @GearsAD --- src/navability/services/loader.py | 82 ++++++++++++++++++------------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/src/navability/services/loader.py b/src/navability/services/loader.py index a4d16e9..3f52c16 100644 --- a/src/navability/services/loader.py +++ b/src/navability/services/loader.py @@ -2,8 +2,10 @@ import toml import re -from gql import gql +from gql import gql # used to parse GraphQL queries +from graphql import DocumentNode, GraphQLSyntaxError # used to handle GraphQL errors +# Define a class to represent a GraphQL fragment with a name and data class FragmentData: def __init__(self, name: str, data: str): self.name = name @@ -12,6 +14,7 @@ def __init__(self, name: str, data: str): def __str__(self) -> str: return f"{self.name}\n{self.data}" +# Define a class to represent a group of related GraphQL fragments with a common name class Fragment: def __init__(self, name: str, data: list[FragmentData]): self.name = name @@ -20,24 +23,27 @@ def __init__(self, name: str, data: list[FragmentData]): def __str__(self) -> str: return f"{self.name}:\n" + "\n".join([str(d) for d in self.data]) -class Query: - def __init__(self, name: str, data: str): - self.name = name +# Define a class to represent a GraphQL operation (query, mutation, or subscription) with a type and data +class Operation: + def __init__(self, operation_type: str, data: str): + self.operation_type = operation_type self.data = data def __str__(self) -> str: - return f"{self.name}:\n{self.data}" + return f"{self.operation_type}:\n{self.data}" +# Define a function to get all files with a given extension from a given folder path def get_files(folder_path: str, extension: str) -> list[str]: - files = [] - for file in os.listdir(folder_path): - file_path = os.path.join(folder_path, file) - if os.path.isdir(file_path): - files += get_files(file_path, extension) - elif file.endswith(extension): - files.append(file_path) - return files - + files = [] + for file in os.listdir(folder_path): + file_path = os.path.join(folder_path, file) + if os.path.isdir(file_path): + files += get_files(file_path, extension) + elif file.endswith(extension): + files.append(file_path) + return files + +# Define a function to extract the name of a fragment from its string representation def get_fragment_name(fragment_string: str) -> str: pattern = r"fragment\s+(\S+)\s+on" match = re.search(pattern, fragment_string) @@ -46,11 +52,12 @@ def get_fragment_name(fragment_string: str) -> str: else: return None -def get_queries(folder_path: str) -> dict[str, Query]: +# Define a function to read all TOML files in a given folder path and return a dictionary of operation names mapped to their corresponding Operation objects +def get_operations(folder_path: str) -> dict[str, Operation]: files = get_files(folder_path, ".toml") fragments = [] - queries = {} + operations = {} for file in files: with open(file, "r") as f: @@ -58,6 +65,7 @@ def get_queries(folder_path: str) -> dict[str, Query]: fragment_data_map = {} + # Extract and store all fragments from the TOML file for fragment_data in data["fragment"]: name = fragment_data["name"] fragmentData_objects = [FragmentData(get_fragment_name(d["data"]), d["data"]) for d in fragment_data["data"]] @@ -68,22 +76,30 @@ def get_queries(folder_path: str) -> dict[str, Query]: fragment_data_map[fd.name] = [] fragment_data_map[fd.name].append(fd) - - for query_data in data["queries"]: - name = query_data["name"] - query = Query(name = name, data = query_data["data"]) + # Extract and store all operations from the TOML file + for operation_data in data["operation"]: + name = operation_data["name"] + operation = Operation(operation_type = "", data = operation_data["data"]) for fd_name, fd_list in fragment_data_map.items(): - if fd_name in query_data["data"]: + if fd_name in operation_data["data"]: for fd in fd_list: - query.data += "\n" + "\n" + fd.data - # GQL interpret the query/mutation - query.data = gql(query.data) - # Add to dictionary - queries[name] = query - - return queries - -__all__ = ['get_queries'] - -# Load and export -GQL_QUERIES = get_queries(os.path.join(".", "sdkCommonGQL")) \ No newline at end of file + operation.data += "\n" + "\n" + fd.data + try: + # Parse the operation data using the gql function and set the operation type + + # [Alucard] @GearsAD So we need to make a choice here, either we have Operation contain just a DocumentNode + # or we just use a string, but I'll leave that to you. + parsed_data = gql(operation.data) + operation.operation_type = parsed_data.definitions[0].operation + operations[name] = operation + except GraphQLSyntaxError as e: + # If there is an error parsing the operation data, print an error message + print(f"Error: Error parsing operation data: {e} \n {operation.data}") + + return operations + +# Define a list of symbols to export from this module +__all__ = ['get_operations'] + +# Load all GraphQL operations from the "sdkCommonGQL" folder and export them +GQL_OPERATIONS = get_operations(os.path.join(".", "sdkCommonGQL")) \ No newline at end of file From 1a6d83a730de2709b3f852493b30dc508ccdd639 Mon Sep 17 00:00:00 2001 From: Sam Claassens Date: Tue, 11 Apr 2023 08:33:30 -0500 Subject: [PATCH 6/8] Implementing GQL loader --- .vscode/settings.json | 3 +- sdkCommonGQL | 2 +- setup.py | 3 +- src/navability/entities/client.py | 14 ++-- src/navability/services/__init__.py | 1 + src/navability/services/loader.py | 109 ++++++++++++++++------------ src/navability/services/variable.py | 40 ++-------- 7 files changed, 82 insertions(+), 90 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index af7446d..19482bf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "python.analysis.extraPaths": [ "./src" - ] + ], + "python.formatting.provider": "black" } \ No newline at end of file diff --git a/sdkCommonGQL b/sdkCommonGQL index b8f4edb..5d71eb1 160000 --- a/sdkCommonGQL +++ b/sdkCommonGQL @@ -1 +1 @@ -Subproject commit b8f4edb85bb4aa0c2b2bc04e1492e5bebf7bbfcf +Subproject commit 5d71eb16e8645b740a93d78818f7c397fead058a diff --git a/setup.py b/setup.py index 43ef114..983798b 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ """ ) -_version = "0.5.1" +_version = "0.6.0" setup( name="navabilitysdk", @@ -38,5 +38,6 @@ "flake8==4.0.1", "pytest==6.2.5", "pytest-asyncio==0.18.1", + "pyyaml==6.0", ], ) diff --git a/src/navability/entities/client.py b/src/navability/entities/client.py index bade1fa..807af04 100644 --- a/src/navability/entities/client.py +++ b/src/navability/entities/client.py @@ -5,12 +5,12 @@ @dataclass() class Client: - userId: str - robotId: str - sessionId: str + userLabel: str + robotLabel: str + sessionLabel: str def __repr__(self): - return f"" # noqa: E501, B950 + return f"" # noqa: E501, BLabeLabel def dump(self): return ClientSchema().dump(self) @@ -24,9 +24,9 @@ def load(data): class ClientSchema(Schema): - userId = fields.String(required=True) - robotId = fields.String(required=True) - sessionId = fields.String(required=True) + userLabel = fields.String(required=True) + robotLabel = fields.String(required=True) + sessionLabel = fields.String(required=True) class Meta: ordered = True diff --git a/src/navability/services/__init__.py b/src/navability/services/__init__.py index f059f44..933ed7e 100644 --- a/src/navability/services/__init__.py +++ b/src/navability/services/__init__.py @@ -6,3 +6,4 @@ from .utils import * from .variable import * from .blob import * +from .loader import * diff --git a/src/navability/services/loader.py b/src/navability/services/loader.py index 3f52c16..36ba211 100644 --- a/src/navability/services/loader.py +++ b/src/navability/services/loader.py @@ -1,28 +1,43 @@ +from __future__ import annotations import os import toml import re from gql import gql # used to parse GraphQL queries -from graphql import DocumentNode, GraphQLSyntaxError # used to handle GraphQL errors +from graphql import GraphQLSyntaxError # used to handle GraphQL errors # Define a class to represent a GraphQL fragment with a name and data -class FragmentData: +class Fragment: def __init__(self, name: str, data: str): self.name = name self.data = data - def __str__(self) -> str: - return f"{self.name}\n{self.data}" - -# Define a class to represent a group of related GraphQL fragments with a common name -class Fragment: - def __init__(self, name: str, data: list[FragmentData]): - self.name = name - self.data = data + """ + Get the dependencies for this fragment + """ + def generate_dependencies(self, all_fragments: dict[str, Fragment]) -> list[Fragment]: + # Pattern for any fragment starting with an ellipsis + pattern = r"\.{3}[a-zA-Z_]*[ \n\r]" + dependent_fragment_names = [match[3:-1] for match in re.findall(pattern, self.data)] + for d in dependent_fragment_names: + if all_fragments.get(d, None) == None: + raise Exception(f"Query ${self.name} uses fragment ${d} and this fragment does not exist.") + self.dependent_fragments = [all_fragments[d] for d in dependent_fragment_names] + return self.dependent_fragments + # Define a function to extract the name of a fragment from its string representation + def get_fragment_name(self) -> str: + pattern = r"fragment\s+(\S+)\s+on" + match = re.search(pattern, self.data) + if match: + return match.group(1) + else: + return None + def __str__(self) -> str: return f"{self.name}:\n" + "\n".join([str(d) for d in self.data]) + # Define a class to represent a GraphQL operation (query, mutation, or subscription) with a type and data class Operation: def __init__(self, operation_type: str, data: str): @@ -32,6 +47,7 @@ def __init__(self, operation_type: str, data: str): def __str__(self) -> str: return f"{self.operation_type}:\n{self.data}" + # Define a function to get all files with a given extension from a given folder path def get_files(folder_path: str, extension: str) -> list[str]: files = [] @@ -43,63 +59,60 @@ def get_files(folder_path: str, extension: str) -> list[str]: files.append(file_path) return files -# Define a function to extract the name of a fragment from its string representation -def get_fragment_name(fragment_string: str) -> str: - pattern = r"fragment\s+(\S+)\s+on" - match = re.search(pattern, fragment_string) - if match: - return match.group(1) - else: - return None # Define a function to read all TOML files in a given folder path and return a dictionary of operation names mapped to their corresponding Operation objects def get_operations(folder_path: str) -> dict[str, Operation]: files = get_files(folder_path, ".toml") - fragments = [] + fragments = {} operations = {} + # Load all fragments for file in files: with open(file, "r") as f: data = toml.load(f) - fragment_data_map = {} - # Extract and store all fragments from the TOML file - for fragment_data in data["fragment"]: - name = fragment_data["name"] - fragmentData_objects = [FragmentData(get_fragment_name(d["data"]), d["data"]) for d in fragment_data["data"]] - fragment = Fragment(name = name, data = fragmentData_objects) - fragments.append(fragment) - for fd in fragmentData_objects: - if fd.name not in fragment_data_map: - fragment_data_map[fd.name] = [] - fragment_data_map[fd.name].append(fd) - + for (name, frag_string) in data["fragments"].items(): + fragment = Fragment(name = name, data = frag_string) + fragments[name] = fragment + + # Flatten all dependencies + for fragment in fragments.values(): + fragment.generate_dependencies(fragments) + + # Replace all fragment recursion if any exist (expecting "...") + while (any([len(f.dependent_fragments) > 0 for f in fragments.values()])): + # Go through the list until done. + for fragment in fragments.values(): + if len(fragment.dependent_fragments) > 0: # Else ignore. + # If all parents have been resolved, resolve it. + if (all([len(f.dependent_fragments) == 0 for f in fragment.dependent_fragments])): + fragment.data = "\r\n".join([df.data for df in fragment.dependent_fragments]) + "\r\n" + fragment.data + # Clear it + fragment.dependent_fragments = [] + + # Load all operations and include all fragments + for file in files: + with open(file, "r") as f: + data = toml.load(f) # Extract and store all operations from the TOML file - for operation_data in data["operation"]: - name = operation_data["name"] - operation = Operation(operation_type = "", data = operation_data["data"]) - for fd_name, fd_list in fragment_data_map.items(): - if fd_name in operation_data["data"]: - for fd in fd_list: - operation.data += "\n" + "\n" + fd.data + for (name, operation_data) in data["operations"].items(): + operation = Operation(operation_type = "", data = operation_data) + # Include any fragments at the bottom of the query if they are used in the query + for (fd_name, fragment) in fragments.items(): + if fd_name in operation_data: + operation.data += "\n" + "\n" + fragment.data try: # Parse the operation data using the gql function and set the operation type - - # [Alucard] @GearsAD So we need to make a choice here, either we have Operation contain just a DocumentNode - # or we just use a string, but I'll leave that to you. - parsed_data = gql(operation.data) - operation.operation_type = parsed_data.definitions[0].operation + operation.data = gql(operation.data) + operation.operation_type = operation.data.definitions[0].operation operations[name] = operation except GraphQLSyntaxError as e: # If there is an error parsing the operation data, print an error message print(f"Error: Error parsing operation data: {e} \n {operation.data}") - return operations - -# Define a list of symbols to export from this module -__all__ = ['get_operations'] + return (fragments, operations) # Load all GraphQL operations from the "sdkCommonGQL" folder and export them -GQL_OPERATIONS = get_operations(os.path.join(".", "sdkCommonGQL")) \ No newline at end of file +GQL_FRAGMENTS, GQL_OPERATIONS = get_operations(os.path.join(".", "sdkCommonGQL")) diff --git a/src/navability/services/variable.py b/src/navability/services/variable.py index 72b5db0..bfac6d3 100644 --- a/src/navability/services/variable.py +++ b/src/navability/services/variable.py @@ -1,16 +1,7 @@ import logging from typing import List -from gql import gql - -# from navability.common.mutations import GQL_ADDVARIABLE -from navability.services.loader import GQL_QUERIES -# from navability.common.queries import ( -# GQL_FRAGMENT_VARIABLES, -# GQL_LISTVARIABLES, -# GQL_GETVARIABLE, -# GQL_GETVARIABLES, -# ) +from navability.services.loader import GQL_OPERATIONS from navability.entities.client import Client from navability.entities.navabilityclient import ( MutationOptions, @@ -40,7 +31,7 @@ async def _addVariable(navAbilityClient: NavAbilityClient, client: Client, v: Variable): result = await navAbilityClient.mutate( MutationOptions( - GQL_QUERIES["GQL_ADDVARIABLE"].data, + GQL_OPERATIONS["GQL_ADDVARIABLE"].data, {"variable": {"client": client.dump(), "packedData": v.dumpsPacked()}}, ) ) @@ -99,13 +90,13 @@ async def listVariables( List[str]: Async task returning a list of Variable labels. """ params = { - "userId": context.userId, - "robotId": context.robotId, - "sessionId": context.sessionId, + "userLabel": context.userLabel, + "robotLabel": context.robotLabel, + "sessionLabel": context.sessionLabel, } logger.debug(f"Query params: {params}") res = await client.query( - QueryOptions(GQL_QUERIES["GQL_LISTVARIABLES"].data, params) + QueryOptions(GQL_OPERATIONS["QUERY_LIST_VARIABLES"].data, params) ) if ( "users" not in res @@ -127,19 +118,6 @@ async def listVariables( resvar = res['users'][0]['robots'][0]['sessions'][0]['variables'] [vl.append(_lb(v)) for v in resvar] return vl - # # LEGACY - # variables = await getVariables( - # client, - # context, - # detail=QueryDetail.SKELETON, - # regexFilter=regexFilter, - # tags=tags, - # solvable=solvable, - # ) - # result = [v.label for v in variables] - # return result - - # Alias ls = listVariables @@ -178,8 +156,7 @@ async def getVariables( } logger.debug(f"Query params: {params}") res = await client.query( - QueryOptions(GQL_QUERIES["GQL_FRAGMENT_VARIABLES"].data+ GQL_GETVARIABLES), params) - ) + QueryOptions(GQL_OPERATIONS["GQL_GETVARIABLES"].data, params)) logger.debug(f"Query result: {res}") # TODO: Check for errors schema = DETAIL_SCHEMA[detail] @@ -216,8 +193,7 @@ async def getVariable( params["label"] = label logger.debug(f"Query params: {params}") res = await client.query( - QueryOptions(GQL_QUERIES["GQL_FRAGMENT_VARIABLES"].data+ GQL_GETVARIABLE), params) - ) + QueryOptions(GQL_OPERATIONS["GQL_GETVARIABLE"].data, params)) logger.debug(f"Query result: {res}") # TODO: Check for errors # Using the hierarchy approach, we need to check that we have From 4920723c5ad3f174cb2c8b8363ce4518ba5af6de Mon Sep 17 00:00:00 2001 From: Danial Gawryjolek Date: Wed, 12 Apr 2023 00:29:59 +0200 Subject: [PATCH 7/8] update to test for cyclic dependencies - TEST --- src/navability/services/loader.py | 34 +++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/navability/services/loader.py b/src/navability/services/loader.py index 36ba211..47b921c 100644 --- a/src/navability/services/loader.py +++ b/src/navability/services/loader.py @@ -12,9 +12,6 @@ def __init__(self, name: str, data: str): self.name = name self.data = data - """ - Get the dependencies for this fragment - """ def generate_dependencies(self, all_fragments: dict[str, Fragment]) -> list[Fragment]: # Pattern for any fragment starting with an ellipsis pattern = r"\.{3}[a-zA-Z_]*[ \n\r]" @@ -74,20 +71,41 @@ def get_operations(folder_path: str) -> dict[str, Operation]: # Extract and store all fragments from the TOML file for (name, frag_string) in data["fragments"].items(): - fragment = Fragment(name = name, data = frag_string) + fragment = Fragment(name=name, data=frag_string) fragments[name] = fragment # Flatten all dependencies for fragment in fragments.values(): fragment.generate_dependencies(fragments) + # [Alucard] Helper to detect cyclic dependencies - @GearsAD - could add in fragment + def detect_cycle(fragment: Fragment, visited: set[Fragment], path: list[Fragment]) -> bool: + visited.add(fragment) + path.append(fragment) + + for dep in fragment.dependent_fragments: + if dep not in visited: + if detect_cycle(dep, visited, path): + return True + elif dep in path: + print(f"Warning: Cyclic reference detected in fragments: {', '.join([f.name for f in path])}") + return True + + path.pop() + return False + + visited = set() + for fragment in fragments.values(): + if fragment not in visited: + detect_cycle(fragment, visited, []) + # Replace all fragment recursion if any exist (expecting "...") - while (any([len(f.dependent_fragments) > 0 for f in fragments.values()])): + while any([len(f.dependent_fragments) > 0 for f in fragments.values()]): # Go through the list until done. for fragment in fragments.values(): - if len(fragment.dependent_fragments) > 0: # Else ignore. + if len(fragment.dependent_fragments) > 0: # Else ignore. # If all parents have been resolved, resolve it. - if (all([len(f.dependent_fragments) == 0 for f in fragment.dependent_fragments])): + if all([len(f.dependent_fragments) == 0 for f in fragment.dependent_fragments]): fragment.data = "\r\n".join([df.data for df in fragment.dependent_fragments]) + "\r\n" + fragment.data # Clear it fragment.dependent_fragments = [] @@ -98,7 +116,7 @@ def get_operations(folder_path: str) -> dict[str, Operation]: data = toml.load(f) # Extract and store all operations from the TOML file for (name, operation_data) in data["operations"].items(): - operation = Operation(operation_type = "", data = operation_data) + operation = Operation(operation_type="", data=operation_data) # Include any fragments at the bottom of the query if they are used in the query for (fd_name, fragment) in fragments.items(): if fd_name in operation_data: From 89700416b6afcd1cf96e183c5fcba502d39bcaa7 Mon Sep 17 00:00:00 2001 From: Sam Claassens Date: Tue, 11 Apr 2023 16:25:07 -0500 Subject: [PATCH 8/8] Adding a unit test --- tests/common_query_test/test.toml | 46 +++++++++++++++++++++++++++++++ tests/test_loader.py | 24 ++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/common_query_test/test.toml create mode 100644 tests/test_loader.py diff --git a/tests/common_query_test/test.toml b/tests/common_query_test/test.toml new file mode 100644 index 0000000..5cb420d --- /dev/null +++ b/tests/common_query_test/test.toml @@ -0,0 +1,46 @@ +# ================================================= +# Fragments +# ================================================= + +[fragments] + +FRAGMENT_A = """ +fragment FRAGMENT_A on Var { + ...FRAGMENT_B + } +""" + +FRAGMENT_B = """ +fragment FRAGMENT_B on Var { + something + ...FRAGMENT_C + } +""" + +FRAGMENT_C = """ +fragment FRAGMENT_C on Var { + a + } +""" + +FRAGMENT_INDEP = """ +fragment FRAGMENT_INDEP on Var { + something_else + } +""" + + +# ================================================= +# Operations +# ================================================= + +[operations] + +QUERY_A = """ +query QUERY_A($fields_summary: Boolean! = true) { + somethings { + ...FRAGMENT_A + ...FRAGMENT_INDEP @include(if: $fields_summary) + } +} +""" diff --git a/tests/test_loader.py b/tests/test_loader.py new file mode 100644 index 0000000..16d3b58 --- /dev/null +++ b/tests/test_loader.py @@ -0,0 +1,24 @@ +import asyncio + +import pytest +import pathlib + +from navability.services import * + + +@pytest.mark.asyncio +async def test_all_queries_loaded(): + assert len(GQL_OPERATIONS) > 0 + assert len(GQL_FRAGMENTS) > 0 + + +@pytest.mark.asyncio +async def test_mock_queries_loaded(): + fragments, queries = get_operations( + f"{pathlib.Path(__file__).parent.resolve()}/common_query_test" + ) + assert len(fragments) == 4 + assert len(queries) == 1 + # Assert: The tree of fragments are correctly appended to the operation. + # Ew, ugly way to get the original data back, but it's parsed GQL. + assert "fragment FRAGMENT_C on Var" in queries["QUERY_A"].data.loc.source.body