Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V6 against the new API #108

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "sdkCommonGQL"]
path = sdkCommonGQL
url = [email protected]:NavAbility/SDKCommonGQL.git
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"python.analysis.extraPaths": [
"./src"
],
"python.formatting.provider": "black"
}
1 change: 1 addition & 0 deletions sdkCommonGQL
Submodule sdkCommonGQL added at 5d71eb
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"""
)

_version = "0.5.1"
_version = "0.6.0"

setup(
name="navabilitysdk",
Expand All @@ -38,5 +38,6 @@
"flake8==4.0.1",
"pytest==6.2.5",
"pytest-asyncio==0.18.1",
"pyyaml==6.0",
],
)
14 changes: 7 additions & 7 deletions src/navability/entities/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

@dataclass()
class Client:
userId: str
robotId: str
sessionId: str
userLabel: str
robotLabel: str
sessionLabel: str

def __repr__(self):
return f"<Client(userId={self.userId}, robotId={self.robotId}, sessionId={self.sessionId})>" # noqa: E501, B950
return f"<Client(userLabel={self.userLabel}, robotId={self.robotLabel}, sessionId={self.sessionLabel})>" # noqa: E501, BLabeLabel

def dump(self):
return ClientSchema().dump(self)
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/navability/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from .utils import *
from .variable import *
from .blob import *
from .loader import *
136 changes: 136 additions & 0 deletions src/navability/services/loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from __future__ import annotations
import os
import toml
import re

from gql import gql # used to parse GraphQL queries
from graphql import GraphQLSyntaxError # used to handle GraphQL errors

# Define a class to represent a GraphQL fragment with a name and data
class Fragment:
def __init__(self, name: str, data: str):
self.name = name
self.data = data

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):
self.operation_type = operation_type
self.data = data

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 = []
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 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 = {}
operations = {}

# Load all fragments
for file in files:
with open(file, "r") as f:
data = toml.load(f)

# Extract and store all fragments from the TOML file
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)

# [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()]):
# 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 (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
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 (fragments, operations)

# Load all GraphQL operations from the "sdkCommonGQL" folder and export them
GQL_FRAGMENTS, GQL_OPERATIONS = get_operations(os.path.join(".", "sdkCommonGQL"))
39 changes: 8 additions & 31 deletions src/navability/services/variable.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import logging
from typing import List

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.services.loader import GQL_OPERATIONS
from navability.entities.client import Client
from navability.entities.navabilityclient import (
MutationOptions,
Expand Down Expand Up @@ -39,7 +31,7 @@
async def _addVariable(navAbilityClient: NavAbilityClient, client: Client, v: Variable):
result = await navAbilityClient.mutate(
MutationOptions(
gql(GQL_ADDVARIABLE),
GQL_OPERATIONS["GQL_ADDVARIABLE"].data,
{"variable": {"client": client.dump(), "packedData": v.dumpsPacked()}},
)
)
Expand Down Expand Up @@ -98,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(GQL_LISTVARIABLES), params)
QueryOptions(GQL_OPERATIONS["QUERY_LIST_VARIABLES"].data, params)
)
if (
"users" not in res
Expand All @@ -126,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
Expand Down Expand Up @@ -177,8 +156,7 @@ async def getVariables(
}
logger.debug(f"Query params: {params}")
res = await client.query(
QueryOptions(gql(GQL_FRAGMENT_VARIABLES + GQL_GETVARIABLES), params)
)
QueryOptions(GQL_OPERATIONS["GQL_GETVARIABLES"].data, params))
logger.debug(f"Query result: {res}")
# TODO: Check for errors
schema = DETAIL_SCHEMA[detail]
Expand Down Expand Up @@ -215,8 +193,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_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
Expand Down
46 changes: 46 additions & 0 deletions tests/common_query_test/test.toml
Original file line number Diff line number Diff line change
@@ -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)
}
}
"""
24 changes: 24 additions & 0 deletions tests/test_loader.py
Original file line number Diff line number Diff line change
@@ -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