46 |
47 | ## Example
48 |
49 | All you need to do is create objects that inherit from `fastgql.GQL`, which is a simple subclass of `pydantic.BaseModel`. For this example, I am creating a mock schema based on movies. For the functions, you'd usually use a database but I hardcoded the data for this example.
50 |
51 | This code generates a GraphQL schema, reading the object fields and functions. Functions can be sync or async.
52 |
53 | ### Code it
54 |
55 | - Create a file `main.py` with:
56 |
57 | ```py title="main.py"
58 | from uuid import UUID, uuid4
59 | from fastapi import FastAPI
60 | from fastgql import GQL, build_router
61 |
62 | class Account(GQL): # (4)!
63 | id: UUID
64 | username: str
65 |
66 | def watchlist(self) -> list["Movie"]: # (1)!
67 | # Usually you'd use a database to get the user's watchlist. For this example, it is hardcoded.
68 | return [
69 | Movie(id=uuid4(), title="Barbie", release_year=2023),
70 | Movie(id=uuid4(), title="Oppenheimer", release_year=2023),
71 | ]
72 |
73 | def _secret_function(self) -> str: # (2)!
74 | return "this is not exposed!"
75 |
76 | class Person(GQL):
77 | id: UUID
78 | name: str
79 |
80 | def filmography(self) -> list["Movie"]:
81 | return [
82 | Movie(id=uuid4(), title="Barbie", release_year=2023),
83 | Movie(id=uuid4(), title="Wolf of Wallstreet", release_year=2013),
84 | ]
85 |
86 | class Movie(GQL):
87 | id: UUID
88 | title: str
89 | release_year: int
90 |
91 | def actors(self) -> list["Person"]:
92 | return [
93 | Person(id=uuid4(), name="Margot Robbie"),
94 | Person(id=uuid4(), name="Ryan Gosling"),
95 | ]
96 |
97 | class Query(GQL):
98 | def account_by_username(self, username: str) -> Account: # (5)!
99 | # Usually you'd use a database to get this account. For this example, it is hardcoded.
100 | return Account(id=uuid4(), username=username)
101 |
102 | router = build_router(query_models=[Query])
103 |
104 | app = FastAPI() # (3)!
105 |
106 | app.include_router(router, prefix="/graphql")
107 | ```
108 |
109 | 1. Usually this would be a database call. There is an advanced tutorial showing this.
110 | 2. Functions that start with `_` are not included in the GraphQL schema.
111 | 3. This is just a normal FastAPI app. `fastgql.build_router` returns a router that can be included on any FastAPI app.
112 | 4. These objects are subclasses of `pydantic.BaseModel`, so anything you'd want to do with a `BaseModel` you can do with these. You'll see how this comes in handy in future tutorials.
113 | 5. All of these functions can be sync or async. In a future tutorial I'll use an async database call to get data.
114 |
115 | ### Run it
116 |
117 | Run the server with:
118 |
119 |
120 |
121 | ```console
122 | $ uvicorn main:app --reload
123 |
124 | INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
125 | INFO: Started reloader process [28720]
126 | INFO: Started server process [28722]
127 | INFO: Waiting for application startup.
128 | INFO: Application startup complete.
129 | ```
130 |
131 |
132 |
133 |
134 | (Taken from FastAPI docs) About the command uvicorn main:app --reload...
135 |
136 | The command `uvicorn main:app` refers to:
137 |
138 | - `main`: the file `main.py` (the Python "module").
139 | - `app`: the object created inside of `main.py` with the line `app = FastAPI()`.
140 | - `--reload`: make the server restart after code changes. Only do this for development.
141 |
142 |
143 |
144 | ### Check it
145 |
146 | Open your browser at http://127.0.0.1:8000/graphql.
147 |
148 | You will see a GraphiQL UI. This is your homebase for creating GraphQL queries and checking the schema.
149 |
150 | 
151 |
152 | You can see the schema, build queries, and access query history from the icons in the top left respectivly. Here is an example query:
153 |
154 | 
155 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
--------------------------------------------------------------------------------
/docs/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "yaml.schemas": {
3 | "https://squidfunk.github.io/mkdocs-material/schema.json": "file:///Users/jerber/code/open_source/fastgql/fastgql/docs/mkdocs.yml"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/docs/docs/images/account_by_username_query.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerber/fastgql/064c1e846b1758a967d1464c50d7fef6b9f095b9/docs/docs/images/account_by_username_query.png
--------------------------------------------------------------------------------
/docs/docs/images/fastgql shot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerber/fastgql/064c1e846b1758a967d1464c50d7fef6b9f095b9/docs/docs/images/fastgql shot.jpg
--------------------------------------------------------------------------------
/docs/docs/images/graphiql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerber/fastgql/064c1e846b1758a967d1464c50d7fef6b9f095b9/docs/docs/images/graphiql.png
--------------------------------------------------------------------------------
/docs/docs/images/simple_movies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jerber/fastgql/064c1e846b1758a967d1464c50d7fef6b9f095b9/docs/docs/images/simple_movies.png
--------------------------------------------------------------------------------
/docs/docs/img/emoji_logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/docs/index.md:
--------------------------------------------------------------------------------
1 | # FastGQL
2 |
3 | **FastGQL** is a python GraphQL library that uses Pydantic models to build GraphQL types. Think FastAPI for GraphQL.
4 |
5 | ```py
6 | from fastapi import FastAPI
7 | from fastgql import GQL, build_router
8 |
9 | class User(GQL):
10 | name: str
11 | age: int
12 |
13 | class Query(GQL):
14 | def user_by_name(self, name: str) -> User:
15 | return User(name=name, age=27)
16 |
17 | router = build_router(query_models=[Query])
18 |
19 | app = FastAPI()
20 |
21 | app.include_router(router, prefix="/graphql")
22 | ```
23 | I built **FastGQL** because I wanted a GraphQL framework that
24 |
25 | 1. let me use `pydantic.BaseModel` objects to define my schema
26 | 2. let me build dynamic database queries based on incoming requests
27 |
28 | We are now using **FastGQL** in production and have experienced a massive (10x) speedup in average response times because of 2).
29 |
30 | You can find out more about how we build dynamic database queries in the Advanced Tutorial section of the docs.
31 |
32 | ## Installation
33 |
34 |
43 |
44 | ## Example
45 |
46 | All you need to do is create objects that inherit from `fastgql.GQL`, which is a simple subclass of `pydantic.BaseModel`. For this example, I am creating a mock schema based on movies. For the functions, you'd usually use a database but I hardcoded the data for this example.
47 |
48 | This code generates a GraphQL schema, reading the object fields and functions. Functions can be sync or async.
49 |
50 | ### Code it
51 |
52 | - Create a file `main.py` with:
53 |
54 | ```py title="main.py"
55 | from uuid import UUID, uuid4
56 | from fastapi import FastAPI
57 | from fastgql import GQL, build_router
58 |
59 | class Account(GQL): # (4)!
60 | id: UUID
61 | username: str
62 |
63 | def watchlist(self) -> list["Movie"]: # (1)!
64 | # Usually you'd use a database to get the user's watchlist. For this example, it is hardcoded.
65 | return [
66 | Movie(id=uuid4(), title="Barbie", release_year=2023),
67 | Movie(id=uuid4(), title="Oppenheimer", release_year=2023),
68 | ]
69 |
70 | def _secret_function(self) -> str: # (2)!
71 | return "this is not exposed!"
72 |
73 | class Person(GQL):
74 | id: UUID
75 | name: str
76 |
77 | def filmography(self) -> list["Movie"]:
78 | return [
79 | Movie(id=uuid4(), title="Barbie", release_year=2023),
80 | Movie(id=uuid4(), title="Wolf of Wallstreet", release_year=2013),
81 | ]
82 |
83 | class Movie(GQL):
84 | id: UUID
85 | title: str
86 | release_year: int
87 |
88 | def actors(self) -> list["Person"]:
89 | return [
90 | Person(id=uuid4(), name="Margot Robbie"),
91 | Person(id=uuid4(), name="Ryan Gosling"),
92 | ]
93 |
94 | class Query(GQL):
95 | def account_by_username(self, username: str) -> Account: # (5)!
96 | # Usually you'd use a database to get this account. For this example, it is hardcoded.
97 | return Account(id=uuid4(), username=username)
98 |
99 | router = build_router(query_models=[Query])
100 |
101 | app = FastAPI() # (3)!
102 |
103 | app.include_router(router, prefix="/graphql")
104 | ```
105 |
106 | 1. Usually this would be a database call. There is an advanced tutorial showing this.
107 | 2. Functions that start with `_` are not included in the GraphQL schema.
108 | 3. This is just a normal FastAPI app. `fastgql.build_router` returns a router that can be included on any FastAPI app.
109 | 4. These objects are subclasses of `pydantic.BaseModel`, so anything you'd want to do with a `BaseModel` you can do with these. You'll see how this comes in handy in future tutorials.
110 | 5. All of these functions can be sync or async. In a future tutorial I'll use an async database call to get data.
111 |
112 | ### Run it
113 |
114 | Run the server with:
115 |
116 |
117 |
118 | ```console
119 | $ uvicorn main:app --reload
120 |
121 | INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
122 | INFO: Started reloader process [28720]
123 | INFO: Started server process [28722]
124 | INFO: Waiting for application startup.
125 | INFO: Application startup complete.
126 | ```
127 |
128 |
129 |
130 |
131 | (Taken from FastAPI docs) About the command uvicorn main:app --reload...
132 |
133 | The command `uvicorn main:app` refers to:
134 |
135 | - `main`: the file `main.py` (the Python "module").
136 | - `app`: the object created inside of `main.py` with the line `app = FastAPI()`.
137 | - `--reload`: make the server restart after code changes. Only do this for development.
138 |
139 |
140 |
141 | ### Check it
142 |
143 | Open your browser at http://127.0.0.1:8000/graphql.
144 |
145 | You will see a GraphiQL UI. This is your homebase for creating GraphQL queries and checking the schema.
146 |
147 | 
148 |
149 | You can see the schema, build queries, and access query history from the icons in the top left respectivly. Here is an example query:
150 |
151 | 
152 |
--------------------------------------------------------------------------------
/docs/docs/stylesheets/extra.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --md-primary-fg-color: #8DA4EF;
3 | /* --md-primary-fg-color--light: #C0C0C0; */
4 | /* --md-primary-fg-color--dark: #80030C; */
5 |
6 | --md-accent-fg-color: #AB4ABA;
7 | /* --md-accent-fg-color--transparent: hsla(#{hex2hsl($clr-indigo-a200)}, 0.1); */
8 | /* --md-accent-bg-color: hsla(0, 0%, 100%, 1); */
9 | /* --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7); */
10 | }
11 |
--------------------------------------------------------------------------------
/docs/docs/tutorial/index.md:
--------------------------------------------------------------------------------
1 | # Intro, Installation, and First Steps
2 |
3 | This tutorial assumes you know the basics of GraphQL. If you don't, I suggest checking out this great [tutorial](https://graphql.org/learn/) first.
4 |
5 | ### Create a project and install FastGQL
6 |
7 | Create a folder:
8 |
9 | ```bash
10 | mkdir fastgql-tutorial
11 | cd fastgql-tutorial
12 | ```
13 |
14 | Now, we'll create a virtual environment. This allows us to install python libraries scoped to this project.
15 |
16 | First, ensure you have a python version of 3.10 or greater. You can check this by running:
17 |
18 | ```
19 | python --version
20 | ```
21 |
22 | If you do not have python 3.10 or greater, install that now.
23 |
24 | ```bash
25 | python -m venv virtualenv
26 | ```
27 |
28 | Now we need to activate the virtual environment.
29 |
30 | ```bash
31 | source virtualenv/bin/activate
32 | ```
33 |
34 | Now we can install **FastGQL**:
35 |
36 | ```bash
37 | pip install fastgql
38 | ```
39 |
40 | **FastGQL** installs with [**FastAPI**](https://fastapi.tiangolo.com/) and [**Pydantic V2**](https://docs.pydantic.dev/latest/). You will not need to install a seperate web server library.
41 |
42 | ### Define the Schema
43 |
44 | !!! info
45 | For the sake of simplicity, all code in for this tutorial will be in one file.
46 |
47 |
48 | Full file 👀
49 |
50 | ```Python
51 | {!./docs_src/tutorial/movie_super_simple.py!}
52 | ```
53 |
54 |
55 |
56 | **FastGQL** works by reading objects inheriting from `fastgql.GQL` and constructing a GraphQL schema. It reads both the fields and functions of the object. `GQL` is a simple subclass of `pydantic.BaseModel` and has all of the functionality of a `BaseModel`.
57 |
58 | Even the root `Query` is a `GQL` type.
59 |
60 | Create the file `schema.py`:
61 |
62 | ```python title="schema.py"
63 | from fastapi import FastAPI
64 | from fastgql import GQL, build_router
65 |
66 | class Actor(GQL):
67 | name: str
68 |
69 | class Movie(GQL):
70 | title: str
71 | release_year: int
72 | actors: list[Actor]
73 |
74 | class Query(GQL):
75 | def get_movies(self) -> list[Movie]:
76 | return [
77 | Movie(
78 | title="Barbie",
79 | release_year=2023,
80 | actors=[Actor(name="Margot Robbie")],
81 | )
82 | ]
83 | ```
84 |
85 | ### Build our schema and run it
86 |
87 | Under the hood, **FastGQL** creates a **FastAPI** router that executes incoming GraphQL queries. If you're unfamiliar with **FastAPI**, it is worth checking out their excellent [docs](https://fastapi.tiangolo.com/).
88 |
89 | ```python title="schema.py"
90 | router = build_router(query_models=[Query]) # (1)!
91 |
92 | app = FastAPI() # (2)!
93 |
94 | app.include_router(router, prefix="/graphql") # (3)!
95 | ```
96 |
97 | 1. This is where we build the **FastAPI** router with our schema
98 | 2. Initialize a new FastAPI instance for your app. This can be any app, including one already created elsewhere.
99 | 3. Attach the router to the app and include whatever prefix you'd like the GraphQL endpoint to be reached at.
100 |
101 |
102 | 👀 Full file preview
103 |
104 | ```Python
105 | {!./docs_src/tutorial/movie_super_simple.py!}
106 | ```
107 |
108 |
109 |
110 | The easiest way to run this **FastAPI** server is with [**Uvicorn**](https://www.uvicorn.org/), which is a fast async web server.
111 |
112 | ```bash
113 | uvicorn schema:app --reload # (1)!
114 | ```
115 |
116 | 1. A good explaination of **Uvicorn** can be found [here](https://fastapi.tiangolo.com/#run-it).
117 |
118 | ### Query it
119 |
120 | It is time to execute your first query! Go to `http://0.0.0.0:8000/graphql` where you should see a GUI for GraphQL called GraphiQL.
121 |
122 | 
123 |
124 | Paste this query into the text box and hit the play button:
125 |
126 | ```graphql
127 | {
128 | getMovies {
129 | title
130 | releaseYear
131 | actors {
132 | name
133 | }
134 | }
135 | }
136 | ```
137 |
138 | You should see the data we made in `schema.py` come back 🎉
139 |
140 | 
141 |
142 | Now, we will move on to more advanced tutorials!
--------------------------------------------------------------------------------
/docs/docs/tutorial/more_advanced.md:
--------------------------------------------------------------------------------
1 | # Advanced Tutorial
2 |
3 | !!! tip
4 | I am still working on the docs. Feel free to DM me on Twitter if you have questions or feedback.
5 |
6 | The reason I built **FastGQL** was to address the `n+1` problem I had with other frameworks.
7 |
8 | Consider this working example using [`EdgeDB`](https://edgedb.com) as the database:
9 |
10 |
11 | Full file 👀
12 |
13 | ```Python
14 | {!./docs_src/tutorial/movies_edgedb.py!}
15 | ```
16 |
17 |
18 |
19 | For each connection, we have to make a new query. For example, if your query is:
20 |
21 | ```graphql
22 | {
23 | accountByUsername(username: "Cameron") {
24 | id
25 | username
26 | watchlist(limit: 100) {
27 | __typename
28 | ...on Movie {
29 | id
30 | title
31 | releaseYear
32 | actors {
33 | name
34 | }
35 | }
36 | ... on Show {
37 | id
38 | title
39 | }
40 | }
41 | }
42 | }
43 | ```
44 |
45 | You get back a lot of nested data. For each nested data you get, that's another database call. For example, to get actors from a movie:
46 |
47 | ```python
48 | class Content(GQLInterface):
49 | id: UUID
50 | title: str
51 |
52 | async def actors(self) -> list["Person"]:
53 | q = """select Content { actors: { id, name } } filter .id = $id"""
54 | content_d = await query_required_single_json(
55 | name="content.actors", query=q, id=self.id
56 | )
57 | return [Person(**p) for p in content_d["actors"]]
58 | ```
59 |
60 | So, to execute this query, the server had to:
61 | 1) get the account by username from the database,
62 | 2) get the watchlist of that user from the database,
63 | 3) get the actor of each movie from the database
64 |
65 | There are some solutions to make this process more efficient. One of them is using [dataloaders](https://xuorig.medium.com/the-graphql-dataloader-pattern-visualized-3064a00f319f).
66 |
67 | However, even with a dataloader, you are still making new requests to the database for each new level of data you are requesting.
68 |
69 | **FastGQL** comes with a way to solve this problem. It ships with `QueryBuilder` functionality. This allows you to map your GraphQL schema to your database schema, which means you can dynamically generate the exact database query you need to fulfill the client's request.
70 |
71 | !!! note
72 | Currently `QueryBuilder` only works with `EdgeDB`.
73 |
74 | Here is a full example of the same schema, now using the `QueryBuilder` feature.
75 |
76 |
77 | Full file 👀
78 |
79 | ```Python
80 | {!./docs_src/tutorial/movies_qb.py!}
81 | ```
82 |
83 |
84 |
85 | Now this same query:
86 | ```graphql
87 | {
88 | accountByUsername(username: "Cameron") {
89 | id
90 | username
91 | watchlist(limit: 100) {
92 | __typename
93 | ...on Movie {
94 | id
95 | title
96 | releaseYear
97 | actors {
98 | name
99 | }
100 | }
101 | ... on Show {
102 | id
103 | title
104 | }
105 | }
106 | }
107 | }
108 | ```
109 | executes with only one call to the database that looks like this:
110 | ```
111 | select Account { id, username, watchlist: { typename := .__type__.name, Movie := (select [is Movie] { __typename := .__type__.name, id, release_year, title, actors: { name } }), Show := (select [is Show] { __typename := .__type__.name, id, title }) } LIMIT $limit } filter .username = $username
112 | ```
113 |
114 | The original query took around 180ms to execute and make 6 database calls.
115 |
116 | The new query using `QueryBuilders` takes less than 30ms to execute and only makes one database call!
117 |
118 | For this small example, the results are not so dramatic. But in production, on large datasets, the speed advantage can easily be 10x.
--------------------------------------------------------------------------------
/docs/docs/tutorial/query_builder.md:
--------------------------------------------------------------------------------
1 | # Query Builder Tutorial
2 |
3 | I will be adding this later. Please dm me on twitter if you'd like me to make it sooner.
--------------------------------------------------------------------------------
/docs/docs_src/build_data.py:
--------------------------------------------------------------------------------
1 | from uuid import uuid4, UUID
2 |
3 | people = [
4 | {"id": uuid4(), "name": "Margot Robbie"},
5 | {"id": uuid4(), "name": "Ryan Gosling"},
6 | {"id": uuid4(), "name": "Jeremy Allen White"},
7 | ]
8 | movies = [
9 | {"id": uuid4(), "title": "Barbie", "release_year": 2023, "actors": people[0:2]}
10 | ]
11 | shows = [
12 | {
13 | "id": uuid4(),
14 | "title": "Game Of Thrones",
15 | "seasons": [{"id": uuid4(), "number": x} for x in range(1, 9)],
16 | "actors": people[2:],
17 | }
18 | ]
19 | content = [*movies, *shows]
20 | content_by_person_id = {}
21 | for p in people:
22 | person_id = p["id"]
23 | filmography = []
24 | for c in content:
25 | if person_id in [c_actor["id"] for c_actor in c["actors"]]:
26 | filmography.append(c)
27 | content_by_person_id[person_id] = filmography
28 | movies_by_id: dict[UUID, dict] = {m["id"]: m for m in movies}
29 | shows_by_id: dict[UUID, dict] = {s["id"]: s for s in shows}
30 | content_by_id: dict[UUID, dict] = {c["id"]: c for c in content}
31 | accounts = [{"id": uuid4(), "username": "jeremy", "watchlist": content}]
32 | accounts_by_username = {a["username"]: a for a in accounts}
33 |
--------------------------------------------------------------------------------
/docs/docs_src/movies.py:
--------------------------------------------------------------------------------
1 | import typing as T
2 | from uuid import UUID
3 | from pydantic import TypeAdapter
4 | from fastapi import FastAPI
5 | from fastgql import GQL, GQLInterface, build_router
6 | from .build_data import (
7 | accounts_by_username,
8 | content_by_person_id,
9 | content_by_id,
10 | shows_by_id,
11 | )
12 |
13 | Contents = list[T.Union["Movie", "Show"]]
14 |
15 |
16 | class Account(GQL):
17 | id: UUID
18 | username: str
19 |
20 | def watchlist(self) -> Contents:
21 | """create a list of movies and shows"""
22 | watchlist_raw = accounts_by_username[self.username]["watchlist"]
23 | return TypeAdapter(Contents).validate_python(watchlist_raw)
24 |
25 |
26 | class Person(GQL):
27 | id: UUID
28 | name: str
29 |
30 | def filmography(self) -> Contents:
31 | return TypeAdapter(Contents).validate_python(content_by_person_id[self.id])
32 |
33 |
34 | class Content(GQLInterface):
35 | id: UUID
36 | title: str
37 |
38 | def actors(self) -> list["Person"]:
39 | return [Person(**p) for p in content_by_id[self.id]["actors"]]
40 |
41 |
42 | class Movie(Content):
43 | id: UUID
44 | release_year: int
45 |
46 |
47 | class Show(Content):
48 | id: UUID
49 |
50 | def seasons(self) -> list["Season"]:
51 | return [Season(**s) for s in shows_by_id[self.id]["seasons"]]
52 |
53 | def num_seasons(self) -> int:
54 | return len(self.seasons())
55 |
56 |
57 | class Season(GQL):
58 | id: UUID
59 | number: int
60 | show: "Show"
61 |
62 |
63 | class Query(GQL):
64 | @staticmethod
65 | async def account_by_username(username: str) -> Account:
66 | account = accounts_by_username[username]
67 | return Account(**account)
68 |
69 |
70 | router = build_router(query_models=[Query])
71 |
72 | app = FastAPI()
73 |
74 | app.include_router(router, prefix="/graphql")
75 |
--------------------------------------------------------------------------------
/docs/docs_src/tutorial/movie_super_simple.py:
--------------------------------------------------------------------------------
1 | from fastapi import FastAPI
2 | from fastgql import GQL, build_router
3 |
4 |
5 | class Actor(GQL):
6 | name: str
7 |
8 |
9 | class Movie(GQL):
10 | title: str
11 | release_year: int
12 | actors: list[Actor]
13 |
14 |
15 | class Query(GQL):
16 | def get_movies(self) -> list[Movie]:
17 | return [
18 | Movie(
19 | title="Barbie",
20 | release_year=2023,
21 | actors=[Actor(name="Margot Robbie")],
22 | )
23 | ]
24 |
25 |
26 | router = build_router(query_models=[Query])
27 |
28 | app = FastAPI()
29 |
30 | app.include_router(router, prefix="/graphql")
31 |
--------------------------------------------------------------------------------
/docs/docs_src/tutorial/movies_edgedb.py:
--------------------------------------------------------------------------------
1 | import json
2 | import time
3 | import typing as T
4 | from uuid import UUID
5 | import edgedb
6 | from pydantic import TypeAdapter
7 | from fastapi import FastAPI
8 | from fastgql import GQL, GQLInterface, build_router
9 | from dotenv import load_dotenv
10 |
11 | load_dotenv()
12 |
13 | edgedb_client = edgedb.create_async_client()
14 |
15 | Contents = list[T.Union["Movie", "Show"]]
16 |
17 |
18 | async def query_required_single_json(
19 | name: str, query: str, **variables
20 | ) -> dict[str, T.Any]:
21 | start = time.time()
22 | res = json.loads(
23 | await edgedb_client.query_required_single_json(query=query, **variables)
24 | )
25 | took_ms = round((time.time() - start) * 1_000, 2)
26 | print(f"[{name}] took {took_ms} ms")
27 | return res
28 |
29 |
30 | class Account(GQL):
31 | id: UUID
32 | username: str
33 |
34 | async def watchlist(self, limit: int) -> Contents:
35 | q = """select Account {
36 | watchlist: { id, title, release_year := [is Movie].release_year } limit $limit
37 | } filter .id = $id"""
38 | account_d = await query_required_single_json(
39 | name="account.watchlist", query=q, id=self.id, limit=limit
40 | )
41 | return TypeAdapter(Contents).validate_python(account_d["watchlist"])
42 |
43 |
44 | class Person(GQL):
45 | id: UUID
46 | name: str
47 |
48 | async def filmography(self) -> Contents:
49 | q = """select Person {
50 | filmography: { id, title, release_year := [is Movie].release_year }
51 | } filter .id = $id"""
52 | person_d = await query_required_single_json(
53 | name="person.filmography", query=q, id=self.id
54 | )
55 | return TypeAdapter(Contents).validate_python(person_d["filmography"])
56 |
57 |
58 | class Content(GQLInterface):
59 | id: UUID
60 | title: str
61 |
62 | async def actors(self) -> list["Person"]:
63 | q = """select Content { actors: { id, name } } filter .id = $id"""
64 | content_d = await query_required_single_json(
65 | name="content.actors", query=q, id=self.id
66 | )
67 | return [Person(**p) for p in content_d["actors"]]
68 |
69 |
70 | class Movie(Content):
71 | release_year: int
72 |
73 |
74 | class Show(Content):
75 | async def seasons(self) -> list["Season"]:
76 | q = """select Show { season := .$id"""
77 | show_d = await query_required_single_json(
78 | name="show.seasons", query=q, id=self.id
79 | )
80 | return [Season(**s) for s in show_d["season"]]
81 |
82 | async def num_seasons(self) -> int:
83 | q = """select Show { num_seasons } filter .id = $id"""
84 | show_d = await query_required_single_json(
85 | name="show.num_seasons", query=q, id=self.id
86 | )
87 | return show_d["num_seasons"]
88 |
89 |
90 | class Season(GQL):
91 | id: UUID
92 | number: int
93 |
94 | async def show(self) -> Show:
95 | q = """select Season { show: { id, title } } filter .id = $id"""
96 | season_d = await query_required_single_json(
97 | name="season.show", query=q, id=self.id
98 | )
99 | return Show(**season_d["show"])
100 |
101 |
102 | class Query(GQL):
103 | @staticmethod
104 | async def account_by_username(username: str) -> Account:
105 | q = """select Account { id, username } filter .username = $username"""
106 | account_d = await query_required_single_json(
107 | name="account_by_username", query=q, username=username
108 | )
109 | return Account(**account_d)
110 |
111 |
112 | router = build_router(query_models=[Query])
113 |
114 | app = FastAPI()
115 |
116 | app.include_router(router, prefix="/graphql")
117 |
--------------------------------------------------------------------------------
/docs/docs_src/tutorial/movies_qb.py:
--------------------------------------------------------------------------------
1 | import json
2 | import time
3 | import typing as T
4 | from uuid import UUID
5 | import edgedb
6 | from fastapi import FastAPI
7 | from fastgql import (
8 | GQL,
9 | GQLInterface,
10 | build_router,
11 | Link,
12 | Property,
13 | get_qb,
14 | QueryBuilder,
15 | Depends,
16 | Info,
17 | node_from_path,
18 | )
19 | from dotenv import load_dotenv
20 |
21 | load_dotenv()
22 |
23 | edgedb_client = edgedb.create_async_client()
24 |
25 | Contents = list[T.Union["Movie", "Show"]]
26 |
27 |
28 | def parse_raw_content(raw_content: list[dict, T.Any]) -> Contents:
29 | w_list: Contents = []
30 | for item in raw_content:
31 | if item["typename"] == "default::Movie":
32 | if movie := item.get("Movie"):
33 | w_list.append(Movie(**movie))
34 | elif item["typename"] == "default::Show":
35 | if show := item.get("Show"):
36 | w_list.append(Show(**show))
37 | return w_list
38 |
39 |
40 | async def query_required_single_json(
41 | name: str, query: str, **variables
42 | ) -> dict[str, T.Any]:
43 | start = time.time()
44 | res = json.loads(
45 | await edgedb_client.query_required_single_json(query=query, **variables)
46 | )
47 | took_ms = round((time.time() - start) * 1_000, 2)
48 | print(f"[{name}] took {took_ms} ms")
49 | return res
50 |
51 |
52 | class AccountPageInfo(GQL):
53 | has_next_page: bool
54 | has_previous_page: bool
55 | start_cursor: str | None
56 | end_cursor: str | None
57 |
58 |
59 | class AccountEdge(GQL):
60 | cursor: str
61 | node: "Account"
62 |
63 |
64 | class AccountConnection(GQL):
65 | page_info: AccountPageInfo
66 | edges: list[AccountEdge]
67 | total_count: int
68 |
69 |
70 | def update_watchlist(child_qb: QueryBuilder, limit: int) -> None:
71 | child_qb.set_limit(limit)
72 |
73 |
74 | class Account(GQL):
75 | def __init__(self, **data):
76 | super().__init__(**data)
77 | self._data = data
78 |
79 | id: T.Annotated[UUID, Property(db_name="id")] = None
80 | username: T.Annotated[str, Property(db_name="username")] = None
81 |
82 | async def watchlist(
83 | self, info: Info, limit: int
84 | ) -> T.Annotated[Contents, Link(db_name="watchlist", update_qbs=update_watchlist)]:
85 | return parse_raw_content(raw_content=self._data[info.path[-1]])
86 |
87 |
88 | class Content(GQLInterface):
89 | def __init__(self, **data):
90 | super().__init__(**data)
91 | self._data = data
92 |
93 | id: T.Annotated[UUID, Property(db_name="id")] = None
94 | title: T.Annotated[str, Property(db_name="title")] = None
95 |
96 | async def actors(
97 | self, info: Info
98 | ) -> T.Annotated[list["Person"], Link(db_name="actors")]:
99 | return [Person(**p) for p in self._data[info.path[-1]]]
100 |
101 |
102 | class Movie(Content):
103 | release_year: T.Annotated[int, Property(db_name="release_year")] = None
104 |
105 |
106 | class Show(Content):
107 | num_seasons: T.Annotated[int, Property(db_name="num_seasons")] = None
108 |
109 | async def seasons(
110 | self, info: Info
111 | ) -> T.Annotated[list["Season"], Link(db_name=" T.Annotated[Show, Link(db_name="show")]:
124 | return Show(**self._data[info.path[-1]])
125 |
126 |
127 | class Person(GQL):
128 | def __init__(self, **data):
129 | super().__init__(**data)
130 | self._data = data
131 |
132 | id: T.Annotated[UUID, Property(db_name="id")] = None
133 | name: T.Annotated[str, Property(db_name="name")] = None
134 |
135 | async def filmography(
136 | self, info: Info
137 | ) -> T.Annotated[Contents, Link(db_name="filmography")]:
138 | return parse_raw_content(raw_content=self._data[info.path[-1]])
139 |
140 |
141 | AccountEdge.model_rebuild()
142 |
143 |
144 | class Query(GQL):
145 | @staticmethod
146 | async def account_by_username(
147 | username: str, qb: QueryBuilder = Depends(get_qb)
148 | ) -> Account:
149 | s, v = qb.build()
150 | q = f"""select Account {s} filter .username = $username"""
151 | print(q)
152 | account_d = await query_required_single_json(
153 | name="account_by_username", query=q, username=username, **v
154 | )
155 | return Account(**account_d)
156 |
157 | @staticmethod
158 | async def account_connection(
159 | info: Info,
160 | *,
161 | before: str | None = None,
162 | after: str | None = None,
163 | first: int,
164 | ) -> AccountConnection:
165 | qb: QueryBuilder = await Account.qb_config.from_info(
166 | info=info, node=node_from_path(node=info.node, path=["edges", "node"])
167 | )
168 | qb.fields.add("username")
169 | variables = {"first": first}
170 | filter_list: list[str] = []
171 | if before:
172 | filter_list.append(".username > $before")
173 | variables["before"] = before
174 | if after:
175 | filter_list.append(".username < $after")
176 | variables["after"] = after
177 | if filter_list:
178 | filter_s = f'filter {" and ".join(filter_list)} '
179 | else:
180 | filter_s = ""
181 | qb.add_variables(variables, replace=False)
182 | s, v = qb.build()
183 | q = f"""
184 | with
185 | all_accounts := (select Account),
186 | _first := $first,
187 | accounts := (select all_accounts {filter_s}order by .username desc limit _first),
188 | select {{
189 | total_count := count(all_accounts),
190 | accounts := accounts {s}
191 | }}
192 | """
193 | connection_d = await query_required_single_json(
194 | name="account_connection", query=q, **v
195 | )
196 | total_count = connection_d["total_count"]
197 | _accounts = [Account(**d) for d in connection_d["accounts"]]
198 | connection = AccountConnection(
199 | page_info=AccountPageInfo(
200 | has_next_page=len(_accounts) == first and total_count > first,
201 | has_previous_page=after is not None,
202 | start_cursor=_accounts[0].username if _accounts else None,
203 | end_cursor=_accounts[-1].username if _accounts else None,
204 | ),
205 | total_count=total_count,
206 | edges=[
207 | AccountEdge(node=account, cursor=account.username)
208 | for account in _accounts
209 | ],
210 | )
211 | return connection
212 |
213 |
214 | router = build_router(query_models=[Query])
215 |
216 | app = FastAPI()
217 |
218 | app.include_router(router, prefix="/graphql")
219 |
--------------------------------------------------------------------------------
/docs/mkdocs.yml:
--------------------------------------------------------------------------------
1 | # site_name: FastGQL
2 | # theme:
3 | # name: material
4 |
5 | # yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json
6 |
7 | site_name: FastGQL
8 | site_url: https://jerber.github.io/fastgql/
9 | site_description: FastGQL is a python GraphQL framework that is high performance, easy to learn and ready for production.
10 | theme:
11 | name: material
12 | palette:
13 | - scheme: default
14 | primary: custom
15 | accent: custom
16 | toggle:
17 | icon: material/lightbulb
18 | name: Switch to dark mode
19 | - scheme: slate
20 | primary: custom
21 | accent: custom
22 | toggle:
23 | icon: material/lightbulb-outline
24 | name: Switch to light mode
25 | features:
26 | # FROM FASTAPI
27 | - search.suggest
28 | - search.highlight
29 | - navigation.instant
30 | - content.tabs.link
31 | - navigation.indexes
32 | - content.tooltips
33 | # - navigation.path
34 | - content.code.annotate
35 | - content.code.copy
36 | - content.code.select
37 |
38 |
39 | # FROM TUTORIAL
40 | # - navigation.tabs
41 | # - navigation.sections
42 | # - toc.integrate
43 | # - navigation.top
44 | # - search.suggest
45 | # - search.highlight
46 | # - content.tabs.link
47 | # - content.code.annotation
48 | # - content.code.copy
49 | icon:
50 | repo: fontawesome/brands/github-alt
51 | logo: 'img/emoji_logo.svg'
52 | favicon: 'img/emoji_logo.svg'
53 | language: en
54 | repo_name: 'jerber/fastgql'
55 | repo_url: 'https://github.com/jerber/fastgql'
56 | edit_uri: ''
57 | nav:
58 | - FastGQL: index.md
59 | - Tutorial - User Guide:
60 | - tutorial/index.md
61 | - tutorial/more_advanced.md
62 | - tutorial/query_builder.md
63 |
64 | plugins:
65 | - social
66 | - search
67 |
68 | extra:
69 | social:
70 | - icon: fontawesome/brands/github-alt
71 | link: https://github.com/jerber
72 | - icon: fontawesome/brands/twitter
73 | link: https://twitter.com/jerber888
74 | - icon: fontawesome/brands/linkedin
75 | link: https://www.linkedin.com/in/jeremyberman1/
76 |
77 | # markdown_extensions:
78 | # - toc:
79 | # permalink: true
80 | # - pymdownx.superfences:
81 | # custom_fences:
82 | # - name: mermaid
83 | # class: mermaid
84 | # format: !!python/name:pymdownx.superfences.fence_code_format ''
85 | # - pymdownx.tabbed:
86 | # alternate_style: true
87 |
88 | # markdown_extensions:
89 | # - pymdownx.snippets:
90 | # base_path: ['docs']
91 | # check_paths: true
92 |
93 | markdown_extensions:
94 | - pymdownx.highlight:
95 | anchor_linenums: true
96 | line_spans: __span
97 | pygments_lang_class: true
98 | - pymdownx.inlinehilite
99 | - admonition
100 | - pymdownx.arithmatex:
101 | generic: true
102 | - footnotes
103 | - pymdownx.details
104 | - pymdownx.superfences
105 | - pymdownx.tabbed:
106 | alternate_style: true
107 | - attr_list
108 | - abbr
109 | - pymdownx.snippets
110 | - pymdownx.emoji:
111 | emoji_index: !!python/name:material.extensions.emoji.twemoji
112 | emoji_generator: !!python/name:materialx.emoji.to_svg
113 | - mdx_include
114 |
115 | # this is SQLMODEL
116 | # markdown_extensions:
117 | # - toc:
118 | # permalink: true
119 | # - markdown.extensions.codehilite:
120 | # guess_lang: false
121 | # - admonition
122 | # - codehilite
123 | # - extra
124 | # - pymdownx.superfences:
125 | # custom_fences:
126 | # - name: mermaid
127 | # class: mermaid
128 | # format: !!python/name:pymdownx.superfences.fence_code_format ''
129 | # - pymdownx.tabbed:
130 | # alternate_style: true
131 | # - mdx_include
132 |
133 | extra_css:
134 | - stylesheets/extra.css
135 |
--------------------------------------------------------------------------------
/docs/old/fastgql_old.md:
--------------------------------------------------------------------------------
1 | # FastGQL OLD
2 |
3 | FastGQL is a python GraphQL library that uses Pydantic models to build GraphQL types. Think FastAPI for GraphQL.
4 |
5 | ```py title="Pydantic Example" requires="3.10"
6 | from fastapi import FastAPI
7 | from fastgql import GQL, build_router
8 |
9 | class User(GQL):
10 | name: str
11 | age: int
12 |
13 | class Query(GQL):
14 | @staticmethod
15 | def user() -> User:
16 | return User(name="Jeremy", age=27)
17 |
18 | router = build_router(query_models=[Query])
19 |
20 | app = FastAPI()
21 |
22 | app.include_router(router, prefix="/graphql")
23 | ```
24 |
25 | SQLModel is a library for interacting with SQL databases from Python code, with Python objects. It is designed to be intuitive, easy to use, highly compatible, and robust.
26 |
27 | **SQLModel** is based on Python type annotations, and powered by Pydantic and SQLAlchemy.
28 |
29 | The key features are:
30 |
31 | - **Intuitive to write**: Great editor support. Completion everywhere. Less time debugging. Designed to be easy to use and learn. Less time reading docs.
32 | - **Easy to use**: It has sensible defaults and does a lot of work underneath to simplify the code you write.
33 | - **Compatible**: It is designed to be compatible with **FastAPI**, Pydantic, and SQLAlchemy.
34 | - **Extensible**: You have all the power of SQLAlchemy and Pydantic underneath.
35 | - **Short**: Minimize code duplication. A single type annotation does a lot of work. No need to duplicate models in SQLAlchemy and Pydantic.
36 |
37 | ## SQL Databases in FastAPI
38 |
39 |
40 |
41 | **SQLModel** is designed to simplify interacting with SQL databases in FastAPI applications, it was created by the same author. 😁
42 |
43 | It combines SQLAlchemy and Pydantic and tries to simplify the code you write as much as possible, allowing you to reduce the **code duplication to a minimum**, but while getting the **best developer experience** possible.
44 |
45 | **SQLModel** is, in fact, a thin layer on top of **Pydantic** and **SQLAlchemy**, carefully designed to be compatible with both.
46 |
47 | ## Requirements
48 |
49 | A recent and currently supported version of Python.
50 |
51 | As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel.
52 |
53 | ## Installation
54 |
55 |
64 |
65 | ## Example
66 |
67 | For an introduction to databases, SQL, and everything else, see the SQLModel documentation.
68 |
69 | Here's a quick example. ✨
70 |
71 | ### A SQL Table
72 |
73 | Imagine you have a SQL table called `hero` with:
74 |
75 | - `id`
76 | - `name`
77 | - `secret_name`
78 | - `age`
79 |
80 | And you want it to have this data:
81 |
82 | | id | name | secret_name | age |
83 | | --- | ---------- | ---------------- | ---- |
84 | | 1 | Deadpond | Dive Wilson | null |
85 | | 2 | Spider-Boy | Pedro Parqueador | null |
86 | | 3 | Rusty-Man | Tommy Sharp | 48 |
87 |
88 | ### Create a SQLModel Model
89 |
90 | Then you could create a **SQLModel** model like this:
91 |
92 | ```Python
93 | from typing import Optional
94 |
95 | from sqlmodel import Field, SQLModel
96 |
97 |
98 | class Hero(SQLModel, table=True):
99 | id: Optional[int] = Field(default=None, primary_key=True)
100 | name: str
101 | secret_name: str
102 | age: Optional[int] = None
103 | ```
104 |
105 | That class `Hero` is a **SQLModel** model, the equivalent of a SQL table in Python code.
106 |
107 | And each of those class attributes is equivalent to each **table column**.
108 |
109 | ### Create Rows
110 |
111 | Then you could **create each row** of the table as an **instance** of the model:
112 |
113 | ```Python
114 | hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
115 | hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
116 | hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
117 | ```
118 |
119 | This way, you can use conventional Python code with **classes** and **instances** that represent **tables** and **rows**, and that way communicate with the **SQL database**.
120 |
121 | ### Editor Support
122 |
123 | Everything is designed for you to get the best developer experience possible, with the best editor support.
124 |
125 | Including **autocompletion**:
126 |
127 |
128 |
129 | And **inline errors**:
130 |
131 |
132 |
133 | ### Write to the Database
134 |
135 | You can learn a lot more about **SQLModel** by quickly following the **tutorial**, but if you need a taste right now of how to put all that together and save to the database, you can do this:
136 |
137 | ```Python hl_lines="18 21 23-27"
138 | from typing import Optional
139 |
140 | from sqlmodel import Field, Session, SQLModel, create_engine
141 |
142 |
143 | class Hero(SQLModel, table=True):
144 | id: Optional[int] = Field(default=None, primary_key=True)
145 | name: str
146 | secret_name: str
147 | age: Optional[int] = None
148 |
149 |
150 | hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
151 | hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
152 | hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
153 |
154 |
155 | engine = create_engine("sqlite:///database.db")
156 |
157 |
158 | SQLModel.metadata.create_all(engine)
159 |
160 | with Session(engine) as session:
161 | session.add(hero_1)
162 | session.add(hero_2)
163 | session.add(hero_3)
164 | session.commit()
165 | ```
166 |
167 | That will save a **SQLite** database with the 3 heroes.
168 |
169 | ### Select from the Database
170 |
171 | Then you could write queries to select from that same database, for example with:
172 |
173 | ```Python hl_lines="15-18"
174 | from typing import Optional
175 |
176 | from sqlmodel import Field, Session, SQLModel, create_engine, select
177 |
178 |
179 | class Hero(SQLModel, table=True):
180 | id: Optional[int] = Field(default=None, primary_key=True)
181 | name: str
182 | secret_name: str
183 | age: Optional[int] = None
184 |
185 |
186 | engine = create_engine("sqlite:///database.db")
187 |
188 | with Session(engine) as session:
189 | statement = select(Hero).where(Hero.name == "Spider-Boy")
190 | hero = session.exec(statement).first()
191 | print(hero)
192 | ```
193 |
194 | ### Editor Support Everywhere
195 |
196 | **SQLModel** was carefully designed to give you the best developer experience and editor support, **even after selecting data** from the database:
197 |
198 |
199 |
200 | ## SQLAlchemy and Pydantic
201 |
202 | That class `Hero` is a **SQLModel** model.
203 |
204 | But at the same time, ✨ it is a **SQLAlchemy** model ✨. So, you can combine it and use it with other SQLAlchemy models, or you could easily migrate applications with SQLAlchemy to **SQLModel**.
205 |
206 | And at the same time, ✨ it is also a **Pydantic** model ✨. You can use inheritance with it to define all your **data models** while avoiding code duplication. That makes it very easy to use with **FastAPI**.
207 |
208 | ## License
209 |
210 | This project is licensed under the terms of the [MIT license](https://github.com/tiangolo/sqlmodel/blob/main/LICENSE).
211 |
--------------------------------------------------------------------------------
/docs/old/index.md:
--------------------------------------------------------------------------------
1 | # Homepage
2 |
3 | FastGQL is a modern, fast (high-performance), web framework for building GraphQL APIs with Python 3.11+ based on standard Python type hints.
4 |
5 | The key features are:
6 |
7 | - **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance).
8 | - **Fast to code**: Increase the speed to develop features by about 200% to 300%. \*
9 | - **Fewer bugs**: Reduce about 40% of human (developer) induced errors. \*
10 | - **Intuitive**: Great editor support. Completion everywhere. Less time debugging.
11 | - **Easy**: Designed to be easy to use and learn. Less time reading docs.
12 | - **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
13 | - **Robust**: Get production-ready code. With automatic interactive documentation -- via graphiql.
14 |
15 | \* estimation based on tests on an internal development team, building production applications.
16 |
17 | ## Installation
18 |
19 |
40 |
41 | ## Example
42 |
43 | ### Create it
44 |
45 | - Create a file `main.py` with:
46 |
47 | ```Python
48 | from typing import Union
49 |
50 | from fastapi import FastAPI
51 |
52 | app = FastAPI()
53 |
54 |
55 | @app.get("/")
56 | def read_root():
57 | return {"Hello": "World"}
58 |
59 |
60 | @app.get("/items/{item_id}")
61 | def read_item(item_id: int, q: Union[str, None] = None):
62 | return {"item_id": item_id, "q": q}
63 | ```
64 |
65 |
66 | Or use async def...
67 |
68 | If your code uses `async` / `await`, use `async def`:
69 |
70 | ```Python hl_lines="9 14"
71 | from typing import Union
72 |
73 | from fastapi import FastAPI
74 |
75 | app = FastAPI()
76 |
77 |
78 | @app.get("/")
79 | async def read_root():
80 | return {"Hello": "World"}
81 |
82 |
83 | @app.get("/items/{item_id}")
84 | async def read_item(item_id: int, q: Union[str, None] = None):
85 | return {"item_id": item_id, "q": q}
86 | ```
87 |
88 | **Note**:
89 |
90 | If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs.
91 |
92 |
93 |
94 | ### Run it
95 |
96 | Run the server with:
97 |
98 |
99 |
100 | ```console
101 | $ uvicorn main:app --reload
102 |
103 | INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
104 | INFO: Started reloader process [28720]
105 | INFO: Started server process [28722]
106 | INFO: Waiting for application startup.
107 | INFO: Application startup complete.
108 | ```
109 |
110 |
111 |
112 |
113 | About the command uvicorn main:app --reload...
114 |
115 | The command `uvicorn main:app` refers to:
116 |
117 | - `main`: the file `main.py` (the Python "module").
118 | - `app`: the object created inside of `main.py` with the line `app = FastAPI()`.
119 | - `--reload`: make the server restart after code changes. Only do this for development.
120 |
121 |
122 |
123 | ### Check it
124 |
125 | Open your browser at http://127.0.0.1:8000/items/5?q=somequery.
126 |
127 | You will see the JSON response as:
128 |
129 | ```JSON
130 | {"item_id": 5, "q": "somequery"}
131 | ```
132 |
133 | You already created an API that:
134 |
135 | - Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`.
136 | - Both _paths_ take `GET` operations (also known as HTTP _methods_).
137 | - The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`.
138 | - The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`.
139 |
140 | ### Interactive API docs
141 |
142 | Now go to http://127.0.0.1:8000/docs.
143 |
144 | You will see the automatic interactive API documentation (provided by Swagger UI):
145 |
146 | 
147 |
148 | ### Alternative API docs
149 |
150 | And now, go to http://127.0.0.1:8000/redoc.
151 |
152 | You will see the alternative automatic documentation (provided by ReDoc):
153 |
154 | 
155 |
156 | ## Example upgrade
157 |
158 | Now modify the file `main.py` to receive a body from a `PUT` request.
159 |
160 | Declare the body using standard Python types, thanks to Pydantic.
161 |
162 | ```Python hl_lines="4 9-12 25-27"
163 | from typing import Union
164 |
165 | from fastapi import FastAPI
166 | from pydantic import BaseModel
167 |
168 | app = FastAPI()
169 |
170 |
171 | class Item(BaseModel):
172 | name: str
173 | price: float
174 | is_offer: Union[bool, None] = None
175 |
176 |
177 | @app.get("/")
178 | def read_root():
179 | return {"Hello": "World"}
180 |
181 |
182 | @app.get("/items/{item_id}")
183 | def read_item(item_id: int, q: Union[str, None] = None):
184 | return {"item_id": item_id, "q": q}
185 |
186 |
187 | @app.put("/items/{item_id}")
188 | def update_item(item_id: int, item: Item):
189 | return {"item_name": item.name, "item_id": item_id}
190 | ```
191 |
192 | The server should reload automatically (because you added `--reload` to the `uvicorn` command above).
193 |
194 | ### Interactive API docs upgrade
195 |
196 | Now go to http://127.0.0.1:8000/docs.
197 |
198 | - The interactive API documentation will be automatically updated, including the new body:
199 |
200 | 
201 |
202 | - Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API:
203 |
204 | 
205 |
206 | - Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen:
207 |
208 | 
209 |
210 | ### Alternative API docs upgrade
211 |
212 | And now, go to http://127.0.0.1:8000/redoc.
213 |
214 | - The alternative documentation will also reflect the new query parameter and body:
215 |
216 | 
217 |
218 | ### Recap
219 |
220 | In summary, you declare **once** the types of parameters, body, etc. as function parameters.
221 |
222 | You do that with standard modern Python types.
223 |
224 | You don't have to learn a new syntax, the methods or classes of a specific library, etc.
225 |
226 | Just standard **Python 3.7+**.
227 |
228 | For example, for an `int`:
229 |
230 | ```Python
231 | item_id: int
232 | ```
233 |
234 | or for a more complex `Item` model:
235 |
236 | ```Python
237 | item: Item
238 | ```
239 |
240 | ...and with that single declaration you get:
241 |
242 | - Editor support, including:
243 | - Completion.
244 | - Type checks.
245 | - Validation of data:
246 | - Automatic and clear errors when the data is invalid.
247 | - Validation even for deeply nested JSON objects.
248 | - Conversion of input data: coming from the network to Python data and types. Reading from:
249 | - JSON.
250 | - Path parameters.
251 | - Query parameters.
252 | - Cookies.
253 | - Headers.
254 | - Forms.
255 | - Files.
256 | - Conversion of output data: converting from Python data and types to network data (as JSON):
257 | - Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc).
258 | - `datetime` objects.
259 | - `UUID` objects.
260 | - Database models.
261 | - ...and many more.
262 | - Automatic interactive API documentation, including 2 alternative user interfaces:
263 | - Swagger UI.
264 | - ReDoc.
265 |
266 | ---
267 |
268 | Coming back to the previous code example, **FastAPI** will:
269 |
270 | - Validate that there is an `item_id` in the path for `GET` and `PUT` requests.
271 | - Validate that the `item_id` is of type `int` for `GET` and `PUT` requests.
272 | - If it is not, the client will see a useful, clear error.
273 | - Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests.
274 | - As the `q` parameter is declared with `= None`, it is optional.
275 | - Without the `None` it would be required (as is the body in the case with `PUT`).
276 | - For `PUT` requests to `/items/{item_id}`, Read the body as JSON:
277 | - Check that it has a required attribute `name` that should be a `str`.
278 | - Check that it has a required attribute `price` that has to be a `float`.
279 | - Check that it has an optional attribute `is_offer`, that should be a `bool`, if present.
280 | - All this would also work for deeply nested JSON objects.
281 | - Convert from and to JSON automatically.
282 | - Document everything with OpenAPI, that can be used by:
283 | - Interactive documentation systems.
284 | - Automatic client code generation systems, for many languages.
285 | - Provide 2 interactive documentation web interfaces directly.
286 |
287 | ---
288 |
289 | We just scratched the surface, but you already get the idea of how it all works.
290 |
291 | Try changing the line with:
292 |
293 | ```Python
294 | return {"item_name": item.name, "item_id": item_id}
295 | ```
296 |
297 | ...from:
298 |
299 | ```Python
300 | ... "item_name": item.name ...
301 | ```
302 |
303 | ...to:
304 |
305 | ```Python
306 | ... "item_price": item.price ...
307 | ```
308 |
309 | ...and see how your editor will auto-complete the attributes and know their types:
310 |
311 | 
312 |
313 | For a more complete example including more features, see the Tutorial - User Guide.
314 |
315 | **Spoiler alert**: the tutorial - user guide includes:
316 |
317 | - Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**.
318 | - How to set **validation constraints** as `maximum_length` or `regex`.
319 | - A very powerful and easy to use **Dependency Injection** system.
320 | - Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth.
321 | - More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic).
322 | - **GraphQL** integration with Strawberry and other libraries.
323 | - Many extra features (thanks to Starlette) as:
324 | - **WebSockets**
325 | - extremely easy tests based on HTTPX and `pytest`
326 | - **CORS**
327 | - **Cookie Sessions**
328 | - ...and more.
329 |
330 | ## Performance
331 |
332 | Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (\*)
333 |
334 | To understand more about it, see the section Benchmarks.
335 |
336 | ## Optional Dependencies
337 |
338 | Used by Pydantic:
339 |
340 | - email_validator - for email validation.
341 | - pydantic-settings - for settings management.
342 | - pydantic-extra-types - for extra types to be used with Pydantic.
343 |
344 | Used by Starlette:
345 |
346 | - httpx - Required if you want to use the `TestClient`.
347 | - jinja2 - Required if you want to use the default template configuration.
348 | - python-multipart - Required if you want to support form "parsing", with `request.form()`.
349 | - itsdangerous - Required for `SessionMiddleware` support.
350 | - pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI).
351 | - ujson - Required if you want to use `UJSONResponse`.
352 |
353 | Used by FastAPI / Starlette:
354 |
355 | - uvicorn - for the server that loads and serves your application.
356 | - orjson - Required if you want to use `ORJSONResponse`.
357 |
358 | You can install all of these with `pip install "fastapi[all]"`.
359 |
360 | ## License
361 |
362 | This project is licensed under the terms of the MIT license.
363 |
--------------------------------------------------------------------------------
/docs/old/index_old.md:
--------------------------------------------------------------------------------
1 | # Welcome to MkDocs
2 |
3 | For full documentation visit [mkdocs.org](https://www.mkdocs.org).
4 |
5 | ## Commands
6 |
7 | * `mkdocs new [dir-name]` - Create a new project.
8 | * `mkdocs serve` - Start the live-reloading docs server.
9 | * `mkdocs build` - Build the documentation site.
10 | * `mkdocs -h` - Print help message and exit.
11 |
12 | ## Project layout
13 |
14 | mkdocs.yml # The configuration file.
15 | docs/
16 | index.md # The documentation homepage.
17 | ... # Other markdown pages, images and other files.
18 |
--------------------------------------------------------------------------------
/docs/old/tutorial_index_old.md:
--------------------------------------------------------------------------------
1 | # Intro, Installation, and First Steps
2 |
3 | ## Many of these sections were taken from the [SQLModel docs](https://sqlmodel.tiangolo.com/tutorial/), which are very good at explaining Python concepts. I'm including them here to make things easier.
4 |
5 | ## Type hints (From SQLModel)
6 |
7 | If you need a refresher about how to use Python type hints (type annotations), check FastAPI's Python types intro.
8 |
9 | You can also check the mypy cheat sheet.
10 |
11 | **FastGQL** uses type annotations for everything, this way you can use a familiar Python syntax and get all the editor support possible, with autocompletion and in-editor error checking.
12 |
13 | ## Create a Project (from SQLModel)
14 |
15 | Please go ahead and create a directory for the project we will work on on this tutorial.
16 |
17 | What I normally do is that I create a directory named `code` inside my home/user directory.
18 |
19 | And inside of that I create one directory per project.
20 |
21 | So, for example:
22 |
23 |
24 |
25 | ```console
26 | // Go to the home directory
27 | $ cd
28 | // Create a directory for all your code projects
29 | $ mkdir code
30 | // Enter into that code directory
31 | $ cd code
32 | // Create a directory for this project
33 | $ mkdir fastgql-tutorial
34 | // Enter into that directory
35 | $ cd fastgql-tutorial
36 | ```
37 |
38 |
39 |
40 | Make sure you don't name it also `fastgql`, so that you don't end up overriding the name of the package.
41 |
42 | ### Make sure you have Python
43 |
44 | Make sure you have an officially supported version of Python.
45 |
46 | You can check which version you have with:
47 |
48 |
56 |
57 | For now, FastGQL only supports python 3.10 and up.
58 |
59 | If you don't have python 3.10 or up installed, go and install that first.
60 |
61 | ### Create a Python virtual environment (from SQLModel)
62 |
63 | When writing Python code, you should **always** use virtual environments in one way or another.
64 |
65 | If you don't know what that is, you can read the official tutorial for virtual environments, it's quite simple.
66 |
67 | In very short, a virtual environment is a small directory that contains a copy of Python and all the libraries you need to run your code.
68 |
69 | And when you "activate" it, any package that you install, for example with `pip`, will be installed in that virtual environment.
70 |
71 | !!! tip " There are other tools to manage virtual environments, like Poetry. "
72 |
73 | And there are alternatives that are particularly useful for deployment like Docker and other types of containers. In this case, the "virtual environment" is not just the Python standard files and the installed packages, but the whole system.
74 |
75 | Go ahead and create a Python virtual environment for this project. And make sure to also upgrade `pip`.
76 |
77 | Here are the commands you could use:
78 |
79 | === "Linux, macOS, Linux in Windows"
80 |
81 |
82 |
83 | ```console
84 | // Remember that you might need to use python3.9 or similar 💡
85 | // Create the virtual environment using the module "venv"
86 | $ python3 -m venv env
87 | // ...here it creates the virtual environment in the directory "env"
88 | // Activate the virtual environment
89 | $ source ./env/bin/activate
90 | // Verify that the virtual environment is active
91 | # (env) $$ which python
92 | // The important part is that it is inside the project directory, at "code/fastgql-tutorial/env/bin/python"
93 | /home/leela/code/fastgql-tutorial/env/bin/python
94 | // Use the module "pip" to install and upgrade the package "pip" 🤯
95 | # (env) $$ python -m pip install --upgrade pip
96 | ---> 100%
97 | Successfully installed pip
98 | ```
99 |
100 |
101 |
102 | === "Windows PowerShell"
103 |
104 |
105 |
106 | ```console
107 | // Create the virtual environment using the module "venv"
108 | # >$ python3 -m venv env
109 | // ...here it creates the virtual environment in the directory "env"
110 | // Activate the virtual environment
111 | # >$ .\env\Scripts\Activate.ps1
112 | // Verify that the virtual environment is active
113 | # (env) >$ Get-Command python
114 | // The important part is that it is inside the project directory, at "code\fastgql-tutorial\env\python.exe"
115 | CommandType Name Version Source
116 | ----------- ---- ------- ------
117 | Application python 0.0.0.0 C:\Users\leela\code\fastgql-tutorial\env\python.exe
118 | // Use the module "pip" to install and upgrade the package "pip" 🤯
119 | # (env) >$ python3 -m pip install --upgrade pip
120 | ---> 100%
121 | Successfully installed pip
122 | ```
123 |
124 |
125 |
126 | ## Install **FastGQL**
127 |
128 | Now, after making sure we are inside of a virtual environment in some way, we can install **FastGQL**:
129 |
130 |