Package: turbulette
TurbuletteTurbulette packages all you need to build great GraphQL APIs :ASGI framework, GraphQL library, ORM and data validationDocumentation :https://turbulette.netlify.appFeatures :Split your API in small, independent applicationsGenerate Pydantic models from GraphQL typesJWT authentication with refresh and fresh tokensDeclarative, powerful and extendable policy-based access control (PBAC)Extendable auth user model with role managementAsync caching (provided by async-caches)Built-in CLI to manage project, apps, and DB migrationsBuilt-in pytest plugin to quickly test your resolversSettings management at project and app-level (thanks to simple-settings)CSRF middleware100% test coverage100% typed, your IDE will thank you ;)Handcrafted with ❤️, from 🇫🇷RequirementsPython 3.6+👍 Turbulette makes use of great tools/frameworks and wouldn't exist without them :Ariadne- Schema-first GraphQL libraryStarlette- The little ASGI framework that shinesGINO- Lightweight, async ORMPydantic- Powerful data validation with type annotationsAlembic- Lightweight database migration toolsimple-settings- A generic settings system inspired by Django's oneasync-caches- Async caching libraryClick- A "Command Line Interface Creation Kit"InstallationpipinstallturbuletteYou will also need an ASGI server, such asuvicorn:pipinstalluvicorn🚀 Quick StartHere is a short example that demonstrates a minimal project setup.We will see how to scaffold a simple Turbulette project, create a Turbulette application, and write some GraphQL schema/resolver. It's advisable to start the project in a virtualenv to isolate your dependencies.
Here we will be usingpoetry:poetryinitThen, install Turbulette from PyPI :poetryaddturbuletteFor the rest of the tutorial, we will assume that commands will be executed under the virtualenv. To spawn a  shell inside the virtualenv, run :poetryshell1: Create a projectFirst, create a directory that will contain the whole project.Now, inside this folder, create your Turbulette project using theturbCLI :turbprojecteshopYou should get with something like this :.└── 📁 eshop├── 📁 alembic│   ├── 📄 env.py│   └── 📄 script.py.mako├── 📄 .env├── 📄 alembic.ini├── 📄 app.py└── 📄 settings.pyLet's break down the structure :📁 eshop: Here is the so-calledTurbulette projectfolder, it will contain applications and project-level configuration files📁 alembic: Contains theAlembicscripts used when generating/applying DB migrations📄 env.py📄 script.py.mako📄 .env: The actual project settings live here📄 app.py: Your API entrypoint, it contains the ASGI app📄 settings.py: Will load settings from.envfileWhy have both.envandsettings.py?You don'thave to. You can also put all your settings insettings.py.
But Turbulette encourage you to follow thetwelve-factor methodology,
that recommend to separate settings from code because config varies substantially across deploys,code does not.
This way, you can untrack.envfrom version control and only keep trackingsettings.py, which will load settings
from.envusing Starlette'sConfigobject.2: Create the first appNow it's time to create a Turbulette application!Run this command under the project directory (eshop) :turbapp--nameaccountYou need to runturb appunder the project dir because the CLI needs to access thealmebic.inifile to create the initial database migration.You should see your new app under the project folder :.└── 📁 eshop...|└── 📁 account├── 📁 graphql├── 📁 migrations│   └── 📄 20200926_1508_auto_ef7704f9741f_initial.py├── 📁 resolvers└── 📄 models.pyDetails :📁 graphql: All the GraphQL schema will live here📁 migrations: Will contain database migrations generated by Alembic📁 resolvers: Python package where you will write resolvers binded to the schema📄 models.py: Will hold GINO models for this appWhat is this "initial" python file under📁 migrations?We won't cover database connection in this quickstart, but note that it's the initial database migration
for theaccountapp that creates its dedicated Alembic branch, needed to generate/apply per-app migrations.Before writing some code, the only thing to do is make Turbulette aware of our lovely account app.To do this, open📄 eshop/settings.pyand add"eshop.account"toINSTALLED_APPS,
so the application is registered and can be picked up by Turbulette at startup :# List installed Turbulette apps that defines some GraphQL schemaINSTALLED_APPS=["eshop.account"]3: GraphQL schemaNow that we have our project scaffold, we can start writing actual schema/code.Create aschema.gqlfile in the📁 graphqlfolder and add this base schema :extendtypeMutation{registerCard(input:CreditCard!):SuccessOut!}inputCreditCard{number:String!expiration:Date!name:String!}typeSuccessOut{success:Booleanerrors:[String]}Note that weextendthe typeMutationbecause Turbulette already defines it. The same goes forQuerytypeNotice that with use theDatescalar, it's one of the custom scalars provided by Turbulette. It parses string in the ISO8601 date format YYY-MM-DD.4: Add pydantic modelWe want to validate ourCreditCardinput to ensure the user has entered a valid card number and date.
Fortunately, Turbulette integrates withPydantic, a data validation library that uses python type annotations,
and offers a convenient way to generate a Pydantic model from a schema type.Create a new📄 pyd_models.pyunder📁 account:fromturbulette.validationimportGraphQLModelfrompydanticimportPaymentCardNumberclassCreditCard(GraphQLModel):classGraphQL:gql_type="CreditCard"fields={"number":PaymentCardNumber}What's happening here?The inheritedGraphQLModelclass is a pydantic model that knows about the GraphQL schema and can produce pydantic fields from a given GraphQL type. We specify the GraphQL type with thegql_typeattribute; it's the only one required.But we also add afieldsattribute to override the type ofnumberfield because it is string typed in our schema. If we don't add this, Turbulette will assume thatnumberis a string and will annotate the number field asstr.fieldsis a mapping between GraphQL field names and the type that will override the schema's one.Let's add another validation check: the expiration date. We want to ensure the user has entered a valid date (i.e., at least greater than now) :fromdatetimeimportdatetimefrompydanticimportPaymentCardNumberfromturbulette.validationimportGraphQLModel,validatorclassCreditCard(GraphQLModel):classGraphQL:gql_type="CreditCard"fields={"number":PaymentCardNumber}@validator("expiration")defcheck_expiration_date(cls,value):ifvalue<datetime.now():raiseValueError("Expiration date is invalid")returnvalueWhy don't we use the@validatorfrom Pydantic?For those who have already used Pydantic, you probably know about the@validatordecorator used add custom validation rules on fields.But here, we use a@validatorimported fromturbulette.validation, why?They're almost identical. Turbulette's validator is just a shortcut to the Pydantic one withcheck_fields=Falseas a default, instead ofTrue, because we use an inheritedBaseModel. The above snippet would correctly work if we used Pydantic's validator and explicitly set@validator("expiration", check_fields=False).5: Add a resolverThe last missing piece is the resolver for ourusermutation, to make the API returning something when querying for it.The GraphQL part is handled byAriadne, a schema-first GraphQL library that allows binding the logic to the schema with minimal code.As you may have guessed, we will create a new Python module in our📁 resolverspackage.Let's call it📄 user.py:fromturbuletteimportmutationfrom..pyd_modelsimportCreditCard@mutation.field("registerCard")asyncdefregister(obj,info,**kwargs):return{"success":True}mutationis the base mutation type defined by Turbulette and is used to register all mutation resolvers (hence the use ofextend type Mutationon the schema).
For now, our resolver is very simple and doesn't do any data validation on inputs and doesn't handle errors.Turbulette has a@validatedecorator that can be used to validate resolver input using a pydantic model (like the one defined inStep 4).Here's how to use it:fromturbuletteimportmutationfrom..pyd_modelsimportCreditCardfromturbulette.validationimportvalidate@mutation.field("registerCard")@validate(CreditCard)asyncdefregister(obj,info,**kwargs):return{"success":True}If the validation succeeds, you can access the validated input data inkwargs["_val_data"]But what happens otherwise? Normally, if the validation fails, pydantic will raise aValidationError,
but here the@validatedecorator handles the exception and will add error messages returned by pydantic into a dedicated error field in the GraphQL response.5: Run itOurregisterCardmutation is now binded to the schema, so let's test it.Start the server in the root directory (the one containing📁 eshopfolder) :uvicorneshop.app:app--port8000Now, go tohttp://localhost:8000/graphql, you will see theGraphQL PlaygroundIDE.
Finally, run theregisterCardmutation, for example :mutationcard{registerCard(input:{number:"4000000000000002"expiration:"2023-05-12"name:"John Doe"}){successerrors}}Should give you the following expected result :{"data":{"registerCard":{"success":true,"errors":null}}}Now, try entering a wrong date (beforenow). You should see the validation error as expected:{"data":{"registerCard":{"success":null,"errors":["expiration: Expiration date is invalid"]}}}How the error message end in theerrorskey?Indeed, we didn't specify anywhere that validation errors should be passed to theerrorskey in ourSuccessOutGraphQL type.
That is because Turbulette has a setting calledERROR_FIELD, which defaults to"errors".
This setting indicates the error field on the GraphLQ output type used by Turbulette when collecting query errors.It means that if you didn't specifyERROR_FIELDon the GraphQL type, you would get an exception telling you that the field is missing.It's the default (and recommended) way of handling errors in Turbulette. Still, as all happens in the@validate, you can always remove it and manually instantiate your Pydantic models in resolvers.Good job! 👏That was a straightforward example, showing off a simple Turbulette API set up. To get the most of it, follow theUser Guide.