Strawberry (Schema first)
The strawberry graphql library is a powerful resolver (implementation) first graphql api library that tightly intergrates with python dataclasses. Through implementing the resolvers strawberry is then able to autogenerate a graphql schema for you.
Contrasting to a resolver first approach, A schema-first approach is a design methodology for building GraphQL APIs in which the schema is the starting point for the development process. In this approach, the schema is used to define the structure of the data that is available through the API, including the types of data that can be queried, the fields and arguments that are available for each type, and any relationships between types.
Once the schema is defined (in the graphql DSL) the implementation of the API can be built to support the schema. This includes defining resolvers for each field in the schema, which are responsible for retrieving the data for that field from the underlying data store.
Turms in this use-case provides the toolbox for generating and migrating changes from your graphql SDL schema to your strawberry python code.
Example
Consider the following schema that you (and your team have designed)
type Beast {
"ID of beast (taken from binomial initial)"
id: ID
"number of legs beast has"
legs: Int
"a beast's name in Latin"
binomial: String
"a beast's name to you and I"
commonName: String
"taxonomy grouping"
taxClass: String
"a beast's prey"
eats: [Beast]
"a beast's predators"
isEatenBy: [Beast]
}
type Query {
"""
get all the beasts on the server
"""
beasts: [Beast]
beast(id: ID!): Beast!
calledBy(commonName: String!): [Beast]!
}
type Mutation {
"""
create a massive beast on the server
"""
createBeast(
id: ID!
legs: Int!
binomial: String!
commonName: String!
taxClass: String!
eats: [ID]
): Beast!
}
type Subscription {
watchBeast(id: ID!): Beast
}
Translating this to a strawberry resolver first approach, is now as simple creating a graphq-config file in the directory using these settings
projects:
default:
schema: beasts.graphql
extensions:
turms:
skip_forwards: true
out_dir: api
stylers:
- type: turms.stylers.capitalize.CapitalizeStyler
- type: turms.stylers.snake_case.SnakeCaseStyler
plugins:
- type: turms.plugins.strawberry.StrawberryPlugin # generates a strawberry schema
processors:
- type: turms.processors.disclaimer.DisclaimerProcessor
- type: turms.processors.black.BlackProcessor
- type: turms.processors.isort.IsortProcessor
- type: turms.processors.merge.MergeProcessor # merges the formated schema with already defined functions
scalar_definitions:
uuid: str
_Any: typing.Any
When running turms gen
This will now generate
""" This file was code generated by turms. If you want to change the contents of this file, you should make sure to add the MergeProcessor to your config will keep your changes when you re-run turms)."""
from enum import Enum
from typing import AsyncGenerator, List, Optional
import strawberry
@strawberry.type
class Beast:
id: Optional[str] = strawberry.field(
description="ID of beast (taken from binomial initial)"
)
legs: Optional[int] = strawberry.field(description="number of legs beast has")
binomial: Optional[str] = strawberry.field(description="a beast's name in Latin")
common_name: Optional[str] = strawberry.field(
description="a beast's name to you and I"
)
tax_class: Optional[str] = strawberry.field(description="taxonomy grouping")
eats: Optional[List[Optional["Beast"]]] = strawberry.field(
description="a beast's prey"
)
is_eaten_by: Optional[List[Optional["Beast"]]] = strawberry.field(
description="a beast's predators"
)
@strawberry.type
class Query:
@strawberry.field(description="get all the beasts on the server")
def beasts(self) -> Optional[List[Optional[Beast]]]:
"""get all the beasts on the server"""
return None
@strawberry.field()
def beast(self, id: str) -> Beast:
return None
@strawberry.field()
def called_by(self, common_name: str) -> List[Optional[Beast]]:
return None
@strawberry.type
class Mutation:
@strawberry.mutation(description="create a massive beast on the server")
def create_beast(
self,
id: str,
legs: int,
binomial: str,
common_name: str,
tax_class: str,
eats: Optional[List[Optional[str]]],
) -> Beast:
"""create a massive beast on the server"""
return None
@strawberry.type
class Subscription:
@strawberry.subscription()
async def watch_beast(self, id: str) -> AsyncGenerator[Optional[Beast], None]:
return None
You can now easily add the resolvers into the generated file. With the (at the time) experimental MergeProcessor migrations in the schema like new fields or change in arguments, can be easily migrated to the python code as function bodies (the resolver part) or other fields that are not schema fields, will not be replaced, but just rerunning
turms gen
Example Project
In the example project (here)[https://github.com/jhnnsrs/turms/tree/master/examples/beasts-strawberry]. We illustrate the necessary configuration to use code generation in a schema first approach.