Skip to content

Instantly share code, notes, and snippets.

@CemraJC
Last active February 10, 2024 11:11
Show Gist options
  • Save CemraJC/6c155444bd5306ac4bcec8ac50e15592 to your computer and use it in GitHub Desktop.
Save CemraJC/6c155444bd5306ac4bcec8ac50e15592 to your computer and use it in GitHub Desktop.
Simple Object Oriented Resolvers in Ariadne
ARIADNE + GRAPHQL: ROBUST BASIS FOR A CODE GENERATOR TO MAKE GRAPHQL SERVERS IN PYTHON
This is a simple example to show how a couple of small wrapper classes can take
Ariadne's relatively non-modular approach, and slide a compatibility layer in to
make it possible to declare classes with methods that map directly to resolvers
for fulfilling a GraphQL schema.
The method names are marked with "@resolver", and the class name should exactly match the
type name in the schema. Each class is defined in its own "model" file, and there are two
special classes for the root queries & mutators.
Each method is expected to take the `obj` and `info` parameters, and return a type that
is conformant with the schema. One example of such a class has been provided.
To make the app, you need to follow the usual procedure, and add the "System()" object
to the `make_executable_schema` call.
------------- FUTURE WORK: -------------
Code generators can use the schema to generate perfect boilerplate code with this structure,
because there is a completely symmetrical pattern-informed match between the GraphQL schema
and the syntax for the python code that defines the resolvers.
That said, the boilerplate generation doesn't properly declare the expecte/default arguments yet,
but that could be easily added without must/any adjustment to the core of this, gql.py
With this 1:1 mapping between an outline of valid python source and the GraphQL schema, generators
don't need to resolve complex relationships (such as with the current ariadne modular repository)
and there is _nothing_ implicit about the relationship between the source code and the schema,
which greatly improves the transparency of the implementation and the security of systems that
rely on it.
from abc import ABC
from ariadne import ObjectType, QueryType, MutationType
#
# Utility class to enable object-oriented resolvers
#
def resolver(func):
"""Custom decorator to mark methods as static resolvers"""
func._is_gql_resolver_ = True
return staticmethod(func)
class BaseResolvers(ABC):
"""Automatically registers resolvers based on method name & decorator.
NOTE: Subclasses must also inherit from an Ariadne type that allows setting the field
"""
def register_resolvers(self):
print("Register", self)
# Iterate over all class methods
for name in dir(self.__class__):
attr = getattr(self.__class__, name)
# Check if the method has been decorated to be a resolver
if hasattr(attr, "_is_gql_resolver_") and attr._is_gql_resolver_:
print(" -", name, attr)
self.set_field(name, attr)
#
# Resolver classes with different bases
#
class ObjectResolvers(ObjectType, BaseResolvers):
"""Python class that allows class structure to mimics the GraphQL schema"""
def __init__(self):
super().__init__(self.__class__.__name__)
self.register_resolvers()
class QueryResolvers(ObjectResolvers):
def __init__(self):
ObjectType.__init__(self, "Query")
self.register_resolvers()
class MutationResolvers(MutationType, BaseResolvers):
def __init__(self):
ObjectType.__init__(self, "Mutation")
self.register_resolvers()
"""
Represents the central node of the system, containing details about the
computer itself, its status, and information related to it.
"""
type System {
"The unique serial ID."
serialID: ID!
"The current operational status of the system."
status: String!
"The current date and time on the system."
dateTime: String!
"The name of the system or device."
name: String!
"The version of Python currently running."
pythonVersion: String!
"The version of the application."
appVersion: String!
"The operating system of the device."
operatingSystem: String!
}
import toml
import platform
import datetime
from ..gql import ObjectResolvers, resolver
class System(ObjectResolvers):
"""
Represents the central node of the system, containing details about the
computer itself, its status, and information related to it.
"""
@resolver
def serialID(obj, info, **data):
return "123456789" # Need actual logic to retrieve the serial ID
@resolver
def status(obj, info, **data):
return obj
@resolver
def dateTime(obj, info, **data):
return datetime.datetime.now()
@resolver
def systemName(obj, info, **data):
return platform.node()
@resolver
def pythonVersion(obj, info, **data):
return platform.python_version()
@resolver
def appVersion(obj, info, **data):
return System._read_pyproject_data()['project']['version']
@resolver
def operatingSystem(obj, info, **data):
return f"{platform.system()} {platform.release()}"
@classmethod
def _read_pyproject_data(_, filepath="pyproject.toml"):
"""
Reads and returns data from the pyproject.toml file.
:param filepath: Path to the pyproject.toml file.
:return: A dictionary containing data from the pyproject.toml file.
"""
try:
with open(filepath, "r", encoding="utf-8") as toml_file:
data = toml.load(toml_file)
return data
except FileNotFoundError:
return f"File not found: {filepath}"
except Exception as e:
return f"An error occurred: {e}"
@CemraJC
Copy link
Author

CemraJC commented Feb 10, 2024

ARIADNE + GRAPHQL: ROBUST BASIS FOR A CODE GENERATOR TO MAKE GRAPHQL SERVERS IN PYTHON

This is a simple example to show how a couple of small wrapper classes can take
Ariadne's completely non-modular approach (functional only), and slide a compatibility
layer in to make it possible to declare classes with methods that map directly to resolvers
for fulfilling a GraphQL schema.

The method names are marked with "@resolver", and the class name should exactly match the
type name in the schema. Each class is defined in its own "model" file, and there are two
special classes for the root queries & mutators.

Each method is expected to take the obj and info parameters, and return a type that
is conformant with the schema. One example of such a class has been provided.

To make the app, you need to follow the usual procedure, and add the "System()" object
to the make_executable_schema call.

                    ------------- FUTURE WORK: -------------

Code generators can use the schema to generate perfect boilerplate code with this structure,
because there is a completely symmetrical pattern-informed match between the GraphQL schema
and the syntax for the python code that defines the resolvers.

With this 1:1 mapping between an outline of valid python source and the GraphQL schema, generators
don't need to resolve complex relationships (such as with the current ariadne modular repository)
and there is nothing implicit about the relationship between the source code and the schema,
which greatly improves the transparency of the implementation and the security of systems that
rely on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment