{"id":15192697,"url":"https://github.com/turbulette/turbulette","last_synced_at":"2025-10-02T08:31:09.379Z","repository":{"id":37942411,"uuid":"279307095","full_name":"turbulette/turbulette","owner":"turbulette","description":"😴 Turbulette - A batteries-included framework to build high performance, fully async GraphQL APIs","archived":true,"fork":false,"pushed_at":"2023-01-09T22:14:15.000Z","size":1402,"stargazers_count":64,"open_issues_count":11,"forks_count":5,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-01-16T10:31:54.231Z","etag":null,"topics":["ariadne","async","async-orm","asyncio","fastapi","framework","gino","graphql","graphql-server","graphql-server-framework","orm","pydantic","python","python3","starlette","uvicorn","web"],"latest_commit_sha":null,"homepage":"https://turbulette.netlify.app","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/turbulette.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":".github/CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-07-13T13:14:21.000Z","updated_at":"2024-12-06T01:11:41.000Z","dependencies_parsed_at":"2023-02-08T14:46:06.900Z","dependency_job_id":null,"html_url":"https://github.com/turbulette/turbulette","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/turbulette%2Fturbulette","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/turbulette%2Fturbulette/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/turbulette%2Fturbulette/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/turbulette%2Fturbulette/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/turbulette","download_url":"https://codeload.github.com/turbulette/turbulette/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234957796,"owners_count":18913349,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ariadne","async","async-orm","asyncio","fastapi","framework","gino","graphql","graphql-server","graphql-server-framework","orm","pydantic","python","python3","starlette","uvicorn","web"],"created_at":"2024-09-27T22:00:35.525Z","updated_at":"2025-10-02T08:31:04.053Z","avatar_url":"https://github.com/turbulette.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Turbulette\n\n\u003cp align=\"center\"\u003e\n\u003ca class=\"badge\" href=\"https://github.com/turbulette/turbulette/actions?query=workflow%3ATest\"\u003e\n    \u003cimg src=\"https://github.com/turbulette/turbulette/workflows/Test/badge.svg\" alt=\"test\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"https://www.codacy.com/gh/turbulette/turbulette/dashboard?utm_source=github.com\u0026utm_medium=referral\u0026utm_content=turbulette/turbulette\u0026utm_campaign=Badge_Coverage\"\u003e\n    \u003cimg src=\"https://app.codacy.com/project/badge/Coverage/e244bb031e044079af419dabd40bb7fc\" alt=\"codacy-coverage\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"https://www.codacy.com/gh/turbulette/turbulette/dashboard?utm_source=github.com\u0026amp;utm_medium=referral\u0026amp;utm_content=turbulette/turbulette\u0026amp;utm_campaign=Badge_Grade\"\u003e\n    \u003cimg src=\"https://app.codacy.com/project/badge/Grade/e244bb031e044079af419dabd40bb7fc\" alt=\"codacy-grade\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"https://pypi.org/project/turbulette/\"\u003e\n    \u003cimg src=\"https://img.shields.io/pypi/v/turbulette\" alt=\"pypi\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"https://img.shields.io/pypi/pyversions/turbulette\"\u003e\n    \u003cimg src=\"https://img.shields.io/pypi/pyversions/turbulette\" alt=\"py-version\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"https://github.com/turbulette/turbulette/blob/main/LICENSE\"\u003e\n    \u003cimg src=\"https://img.shields.io/pypi/l/Turbulette\" alt=\"license\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"http://mypy-lang.org/\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/mypy-checked-blue\" alt=\"mypy\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"https://github.com/psf/black\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/code%20style-black-000000.svg\" alt=\"black\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"https://github.com/PyCQA/bandit\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/security-bandit-yellow.svg\" alt=\"bandit\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"https://pre-commit.com/\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit\u0026logoColor=white\" alt=\"pre-commit\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"https://gitter.im/turbulette/turbulette\"\u003e\n    \u003cimg src=\"https://badges.gitter.im/turbulette/turbulette.svg\" alt=\"gitter\"/\u003e\n\u003c/a\u003e\n\u003ca class=\"badge\" href=\"https://app.netlify.com/sites/turbulette/deploys\"\u003e\n    \u003cimg src=\"https://api.netlify.com/api/v1/badges/3d71e7d8-f219-41c3-9dce-1dc0c5b92251/deploy-status\" alt=\"netlify\"/\u003e\n\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003eTurbulette packages all you need to build great GraphQL APIs :\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\u003cstrong\u003e\u003cem\u003eASGI framework, GraphQL library, ORM and data validation\u003c/em\u003e\u003c/strong\u003e\u003c/p\u003e\n\n---\n\nDocumentation : https://turbulette.netlify.app\n\n---\n\nFeatures :\n\n- Split your API in small, independent applications\n- Generate Pydantic models from GraphQL types\n- JWT authentication with refresh and fresh tokens\n- Declarative, powerful and extendable policy-based access control (PBAC)\n- Extendable auth user model with role management\n- Async caching (provided by async-caches)\n- Built-in CLI to manage project, apps, and DB migrations\n- Built-in pytest plugin to quickly test your resolvers\n- Settings management at project and app-level (thanks to simple-settings)\n- CSRF middleware\n- 100% test coverage\n- 100% typed, your IDE will thank you ;)\n- Handcrafted with ❤️, from 🇫🇷\n\n## Requirements\n\nPython 3.6+\n\n👍 Turbulette makes use of great tools/frameworks and wouldn't exist without them :\n\n- [Ariadne](https://ariadnegraphql.org/) - Schema-first GraphQL library\n- [Starlette](https://www.starlette.io/) - The little ASGI framework that shines\n- [GINO](https://python-gino.org/docs/en/master/index.html) - Lightweight, async ORM\n- [Pydantic](https://pydantic-docs.helpmanual.io/) - Powerful data validation with type annotations\n- [Alembic](https://alembic.sqlalchemy.org/en/latest/index.html) - Lightweight database migration tool\n- [simple-settings](https://github.com/drgarcia1986/simple-settings) - A generic settings system inspired by Django's one\n- [async-caches](https://github.com/rafalp/async-caches) - Async caching library\n- [Click](https://palletsprojects.com/p/click/) - A \"Command Line Interface Creation Kit\"\n\n## Installation\n\n``` bash\npip install turbulette\n```\n\nYou will also need an ASGI server, such as [uvicorn](https://www.uvicorn.org/) :\n\n``` bash\npip install uvicorn\n```\n\n----\n\n## 🚀 Quick Start\n\nHere is a short example that demonstrates a minimal project setup.\n\nWe 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.\nHere we will be using [poetry](https://python-poetry.org/) :\n\n``` bash\npoetry init\n```\n\nThen, install Turbulette from PyPI :\n\n``` bash\npoetry add turbulette\n```\n\nFor the rest of the tutorial, we will assume that commands will be executed under the virtualenv. To spawn a  shell inside the virtualenv, run :\n\n```bash\npoetry shell\n```\n\n### 1: Create a project\n\nFirst, create a directory that will contain the whole project.\n\nNow, inside this folder, create your Turbulette project using the `turb` CLI :\n\n``` bash\nturb project eshop\n```\n\nYou should get with something like this :\n\n```console\n.\n└── 📁 eshop\n    ├── 📁 alembic\n    │   ├── 📄 env.py\n    │   └── 📄 script.py.mako\n    ├── 📄 .env\n    ├── 📄 alembic.ini\n    ├── 📄 app.py\n    └── 📄 settings.py\n```\n\nLet's break down the structure :\n\n- `📁 eshop` : Here is the so-called *Turbulette project* folder, it will contain applications and project-level configuration files\n- `📁 alembic` : Contains the [Alembic](https://alembic.sqlalchemy.org/en/latest/) scripts used when generating/applying DB migrations\n  - `📄 env.py`\n  - `📄 script.py.mako`\n- `📄 .env` : The actual project settings live here\n- `📄 app.py` : Your API entrypoint, it contains the ASGI app\n- `📄 settings.py` : Will load settings from `.env` file\n\n\nWhy have both `.env` and `settings.py`?\n\nYou don't *have to*. You can also put all your settings in `settings.py`.\nBut Turbulette encourage you to follow the [twelve-factor methodology](https://12factor.net),\nthat recommend to separate settings from code because config varies substantially across deploys, *code does not*.\nThis way, you can untrack `.env` from version control and only keep tracking `settings.py`, which will load settings\nfrom `.env` using Starlette's `Config` object.\n\n### 2: Create the first app\n\nNow it's time to create a Turbulette application!\n\nRun this command under the project directory (`eshop`) :\n\n```bash\nturb app --name account\n```\n\nYou need to run `turb app` under the project dir because the CLI needs to access the `almebic.ini` file to create the initial database migration.\n\nYou should see your new app under the project folder :\n\n```console\n.\n└── 📁 eshop\n    ...\n    |\n    └── 📁 account\n        ├── 📁 graphql\n        ├── 📁 migrations\n        │   └── 📄 20200926_1508_auto_ef7704f9741f_initial.py\n        ├── 📁 resolvers\n        └── 📄 models.py\n```\n\nDetails :\n\n- `📁 graphql` : All the GraphQL schema will live here\n- `📁 migrations` : Will contain database migrations generated by Alembic\n- `📁 resolvers` : Python package where you will write resolvers binded to the schema\n- `📄 models.py` : Will hold GINO models for this app\n\nWhat is this \"initial\" python file under `📁 migrations`?\n\nWe won't cover database connection in this quickstart, but note that it's the initial database migration\nfor the `account` app that creates its dedicated Alembic branch, needed to generate/apply per-app migrations.\n\nBefore writing some code, the only thing to do is make Turbulette aware of our lovely account app.\n\nTo do this, open `📄 eshop/settings.py` and add `\"eshop.account\"` to `INSTALLED_APPS`,\nso the application is registered and can be picked up by Turbulette at startup :\n\n``` python\n# List installed Turbulette apps that defines some GraphQL schema\nINSTALLED_APPS = [\"eshop.account\"]\n```\n\n### 3: GraphQL schema\n\nNow that we have our project scaffold, we can start writing actual schema/code.\n\nCreate a `schema.gql` file in the `📁 graphql` folder and add this base schema :\n\n``` graphql\nextend type Mutation {\n    registerCard(input: CreditCard!): SuccessOut!\n}\n\ninput CreditCard {\n    number: String!\n    expiration: Date!\n    name: String!\n}\n\ntype SuccessOut {\n    success: Boolean\n    errors: [String]\n}\n\n```\n\nNote that we *extend* the type `Mutation` because Turbulette already defines it. The same goes for `Query` type\n\nNotice that with use the `Date` scalar, it's one of the custom scalars provided by Turbulette. It parses string in the ISO8601 date format YYY-MM-DD.\n\n### 4: Add pydantic model\n\nWe want to validate our `CreditCard` input to ensure the user has entered a valid card number and date.\nFortunately, Turbulette integrates with [Pydantic](https://pydantic-docs.helpmanual.io/), a data validation library that uses python type annotations,\nand offers a convenient way to generate a Pydantic model from a schema type.\n\nCreate a new `📄 pyd_models.py` under `📁 account` :\n\n```python\nfrom turbulette.validation import GraphQLModel\nfrom pydantic import PaymentCardNumber\n\n\nclass CreditCard(GraphQLModel):\n    class GraphQL:\n        gql_type = \"CreditCard\"\n        fields = {\"number\": PaymentCardNumber}\n```\n\nWhat's happening here?\n\nThe inherited `GraphQLModel` class 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 the `gql_type` attribute; it's the only one required.\n\nBut we also add a `fields` attribute to override the type of `number` field because it is string typed in our schema. If we don't add this, Turbulette will assume that `number` is a string and will annotate the number field as `str`.\n`fields` is a mapping between GraphQL field names and the type that will override the schema's one.\n\nLet'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) :\n\n```python hl_lines=\"3 11 12 13 14 15\"\nfrom datetime import datetime\nfrom pydantic import PaymentCardNumber\nfrom turbulette.validation import GraphQLModel, validator\n\n\nclass CreditCard(GraphQLModel):\n    class GraphQL:\n        gql_type = \"CreditCard\"\n        fields = {\"number\": PaymentCardNumber}\n\n    @validator(\"expiration\")\n    def check_expiration_date(cls, value):\n        if value \u003c datetime.now():\n            raise ValueError(\"Expiration date is invalid\")\n        return value\n```\n\nWhy don't we use the `@validator` from Pydantic?\n\nFor those who have already used Pydantic, you probably know about the `@validator` decorator used add custom validation rules on fields.\n\nBut here, we use a `@validator` imported from `turbulette.validation`, why?\n\nThey're almost identical. Turbulette's validator is just a shortcut to the Pydantic one with `check_fields=False` as a default, instead of `True`, because we use an inherited `BaseModel`. The above snippet would correctly work if we used Pydantic's validator and explicitly set `@validator(\"expiration\", check_fields=False)`.\n\n### 5: Add a resolver\n\nThe last missing piece is the resolver for our `user` mutation, to make the API returning something when querying for it.\n\nThe GraphQL part is handled by [Ariadne](https://ariadnegraphql.org/), a schema-first GraphQL library that allows binding the logic to the schema with minimal code.\n\nAs you may have guessed, we will create a new Python module in our `📁 resolvers` package.\n\nLet's call it `📄 user.py` :\n\n``` python\nfrom turbulette import mutation\nfrom ..pyd_models import CreditCard\n\n@mutation.field(\"registerCard\")\nasync def register(obj, info, **kwargs):\n    return {\"success\": True}\n```\n\n`mutation` is the base mutation type defined by Turbulette and is used to register all mutation resolvers (hence the use of `extend type Mutation` on the schema).\nFor now, our resolver is very simple and doesn't do any data validation on inputs and doesn't handle errors.\n\nTurbulette has a `@validate` decorator that can be used to validate resolver input using a pydantic model (like the one defined in [Step 4](#4-add-pydantic-model)).\n\nHere's how to use it:\n\n``` python hl_lines=\"3 6 7\"\nfrom turbulette import mutation\nfrom ..pyd_models import CreditCard\nfrom turbulette.validation import validate\n\n@mutation.field(\"registerCard\")\n@validate(CreditCard)\nasync def register(obj, info, **kwargs):\n    return {\"success\": True}\n```\n\nIf the validation succeeds, you can access the validated input data in `kwargs[\"_val_data\"]`\nBut what happens otherwise? Normally, if the validation fails, pydantic will raise a `ValidationError`,\nbut here the `@validate` decorator handles the exception and will add error messages returned by pydantic into a dedicated error field in the GraphQL response.\n\n### 5: Run it\n\nOur `registerCard` mutation is now binded to the schema, so let's test it.\n\nStart the server in the root directory (the one containing `📁 eshop` folder) :\n\n```bash\nuvicorn eshop.app:app --port 8000\n```\n\nNow, go to [http://localhost:8000/graphql](http://localhost:8000/graphql), you will see the [GraphQL Playground](https://github.com/graphql/graphql-playground) IDE.\nFinally, run the `registerCard` mutation, for example :\n\n``` graphql\nmutation card {\n  registerCard(\n    input: {\n      number: \"4000000000000002\"\n      expiration: \"2023-05-12\"\n      name: \"John Doe\"\n    }\n  ) {\n    success\n    errors\n  }\n}\n```\n\nShould give you the following expected result :\n\n``` json\n{\n  \"data\": {\n    \"registerCard\": {\n      \"success\": true,\n      \"errors\": null\n    }\n  }\n}\n```\n\nNow, try entering a wrong date (before *now*). You should see the validation error as expected:\n\n```json\n{\n  \"data\": {\n    \"registerCard\": {\n      \"success\": null,\n      \"errors\": [\n        \"expiration: Expiration date is invalid\"\n      ]\n    }\n  }\n}\n```\n\nHow the error message end in the `errors` key?\n\nIndeed, we didn't specify anywhere that validation errors should be passed to the `errors` key in our `SuccessOut` GraphQL type.\nThat is because Turbulette has a setting called `ERROR_FIELD`, which defaults to `\"errors\"`.\nThis setting indicates the error field on the GraphLQ output type used by Turbulette when collecting query errors.\n\nIt means that if you didn't specify `ERROR_FIELD` on the GraphQL type, you would get an exception telling you that the field is missing.\n\nIt'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.\n\nGood job! 👏\n\nThat was a straightforward example, showing off a simple Turbulette API set up. To get the most of it, follow the  [User Guide](https://python-turbulette.github.io/turbulette/user-guide/) .\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fturbulette%2Fturbulette","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fturbulette%2Fturbulette","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fturbulette%2Fturbulette/lists"}