]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
:sparkles: Add docs and tests for encode/databases (#107)
authorSebastián Ramírez <tiangolo@gmail.com>
Mon, 25 Mar 2019 18:17:31 +0000 (22:17 +0400)
committerGitHub <noreply@github.com>
Mon, 25 Mar 2019 18:17:31 +0000 (22:17 +0400)
* :sparkles: Add docs and tests for encode/databases

* :heavy_plus_sign: Add testing-only dependency, databases

Pipfile
Pipfile.lock
docs/img/tutorial/async-sql-databases/image01.png [new file with mode: 0644]
docs/src/async_sql_databases/tutorial001.py [new file with mode: 0644]
docs/tutorial/async-sql-databases.md [new file with mode: 0644]
mkdocs.yml
pyproject.toml
tests/test_tutorial/test_async_sql_databases/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_async_sql_databases/test_tutorial001.py [new file with mode: 0644]

diff --git a/Pipfile b/Pipfile
index 1fd9b869c2fe9b3a364547c75cdbe8da82ca3ab6..bb2af4dddb9ed83c5d918c2976880523256e9ba0 100644 (file)
--- a/Pipfile
+++ b/Pipfile
@@ -27,6 +27,7 @@ uvicorn = "*"
 [packages]
 starlette = "==0.11.1"
 pydantic = "==0.21.0"
+databases = {extras = ["sqlite"],version = "*"}
 
 [requires]
 python_version = "3.6"
index 839342b1b52c1c5a4f0422095a954a41085bfda6..1c100d1f2bf822a07397c514f21633a985531ba1 100644 (file)
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "676c6ae13691eef64abe6638f833cb8a330612521d3fad08718b240328b4877a"
+            "sha256": "24b3b7b88d3cbe671ddbe296e64c15f8558f0e5d5df977200119872a363aac13"
         },
         "pipfile-spec": 6,
         "requires": {
         ]
     },
     "default": {
+        "aiocontextvars": {
+            "hashes": [
+                "sha256:1e0ff5837c8b01c36a1107acdd0baf7853ebdf6c9fc43e8e311f4be37ac2038a",
+                "sha256:6ff7aee14f549d52f0446cbb84d0deddcd3fc677bcf8fbc2ce13f5756d2064dc"
+            ],
+            "markers": "python_version < '3.7'",
+            "version": "==0.2.1"
+        },
+        "aiosqlite": {
+            "hashes": [
+                "sha256:af4fed9e778756fa0ffffc7a8b14c4d7b1a57155dc5669f18e45107313f6019e"
+            ],
+            "version": "==0.9.0"
+        },
+        "contextvars": {
+            "hashes": [
+                "sha256:2341042e1c03a271813e07dba29b6b60fa85c1005ea5ed1638a076cf50b4d625"
+            ],
+            "markers": "python_version < '3.7'",
+            "version": "==2.3"
+        },
+        "databases": {
+            "extras": [
+                "sqlite"
+            ],
+            "hashes": [
+                "sha256:4a0f15669c390a04b439972426350c0ae921ddc08c42bd54f125eb2fb86ee728"
+            ],
+            "index": "pypi",
+            "version": "==0.2.0"
+        },
         "dataclasses": {
             "hashes": [
                 "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f",
             "markers": "python_version < '3.7'",
             "version": "==0.6"
         },
+        "immutables": {
+            "hashes": [
+                "sha256:1e4f4513254ef11e0230a558ee0dcb4551b914993c330005d15338da595d3750",
+                "sha256:228e38dc7a810ba4ff88909908ac47f840e5dc6c4c0da6b25009c626a9ae771c",
+                "sha256:2ae88fbfe1d04f4e5859c924e97313edf70e72b4f19871bf329b96a67ede9ba0",
+                "sha256:2d32b61c222cba1dd11f0faff67c7fb6204ef1982454e1b5b001d4b79966ef17",
+                "sha256:35af186bfac5b62522fdf2cab11120d7b0547f405aa399b6a1e443cf5f5e318c",
+                "sha256:63023fa0cceedc62e0d1535cd4ca7a1f6df3120a6d8e5c34e89037402a6fd809",
+                "sha256:6bf5857f42a96331fd0929c357dc0b36a72f339f3b6acaf870b149c96b141f69",
+                "sha256:7bb1590024a032c7a57f79faf8c8ff5e91340662550d2980e0177f67e66e9c9c",
+                "sha256:7c090687d7e623d4eca22962635b5e1a1ee2d6f9a9aca2f3fb5a184a1ffef1f2",
+                "sha256:bc36a0a8749881eebd753f696b081bd51145e4d77291d671d2e2f622e5b65d2f",
+                "sha256:d9fc6a236018d99af6453ead945a6bb55f98d14b1801a2c229dd993edc753a00"
+            ],
+            "version": "==0.6"
+        },
         "pydantic": {
             "hashes": [
                 "sha256:93fa585402e7c8c01623ea8af6ca23363e8b4c6a020b7a2de9e99fa29d642d50",
             "index": "pypi",
             "version": "==0.21.0"
         },
+        "sqlalchemy": {
+            "hashes": [
+                "sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b"
+            ],
+            "version": "==1.3.1"
+        },
         "starlette": {
             "hashes": [
                 "sha256:9d48b35d1fc7521d59ae53c421297ab3878d3c7cd4b75266d77f6c73cccb78bb"
         },
         "ipython": {
             "hashes": [
-                "sha256:06de667a9e406924f97781bda22d5d76bfb39762b678762d86a466e63f65dc39",
-                "sha256:5d3e020a6b5f29df037555e5c45ab1088d6a7cf3bd84f47e0ba501eeb0c3ec82"
+                "sha256:b038baa489c38f6d853a3cfc4c635b0cda66f2864d136fe8f40c1a6e334e2a6b",
+                "sha256:f5102c1cd67e399ec8ea66bcebe6e3968ea25a8977e53f012963e5affeb1fe38"
             ],
             "markers": "python_version >= '3.3'",
-            "version": "==7.3.0"
+            "version": "==7.4.0"
         },
         "ipython-genutils": {
             "hashes": [
         },
         "isort": {
             "hashes": [
-                "sha256:18c796c2cd35eb1a1d3f012a214a542790a1aed95e29768bdcb9f2197eccbd0b",
-                "sha256:96151fca2c6e736503981896495d344781b60d18bfda78dc11b290c6125ebdb6"
+                "sha256:08f8e3f0f0b7249e9fad7e5c41e2113aba44969798a26452ee790c06f155d4ec",
+                "sha256:4e9e9c4bd1acd66cf6c36973f29b031ec752cbfd991c69695e4e259f9a756927"
             ],
             "index": "pypi",
-            "version": "==4.3.15"
+            "version": "==4.3.16"
         },
         "jedi": {
             "hashes": [
         },
         "mkdocs-material": {
             "hashes": [
-                "sha256:762a71f82c1e291c3ff067cecd9d581557da777332fd98bc0af20fd5ab4a2dd0",
-                "sha256:b2c7174ecaa81fb1d62a5f4906f99fa0e7062ced8f9a14ec4f60b1bef9feebbf"
+                "sha256:0b394aa034b25a09a5874ae2a6ccc426fd81f5764e0991217b169e31cb0c1c0e",
+                "sha256:f5bb80a2c16d045d380edb2c5b05636af1bb709cb859bfaa9d01063a11df803f"
             ],
             "index": "pypi",
-            "version": "==4.0.2"
+            "version": "==4.1.0"
         },
         "more-itertools": {
             "hashes": [
             "hashes": [
                 "sha256:781fb7b9d194ed3fc596b8f0dd4623ff160e3e825dd8c15472376a438c19598b"
             ],
-            "index": "pypi",
             "version": "==1.3.1"
         },
         "terminado": {
         },
         "tornado": {
             "hashes": [
-                "sha256:1a58f2d603476d5e462f7c28ca1dbb5ac7e51348b27a9cac849cdec3471101f8",
-                "sha256:33f93243cd46dd398e5d2bbdd75539564d1f13f25d704cfc7541db74066d6695",
-                "sha256:34e59401afcecf0381a28228daad8ed3275bcb726810654612d5e9c001f421b7",
-                "sha256:35817031611d2c296c69e5023ea1f9b5720be803e3bb119464bb2a0405d5cd70",
-                "sha256:666b335cef5cc2759c21b7394cff881f71559aaf7cb8c4458af5bb6cb7275b47",
-                "sha256:81203efb26debaaef7158187af45bc440796de9fb1df12a75b65fae11600a255",
-                "sha256:de274c65f45f6656c375cdf1759dbf0bc52902a1e999d12a35eb13020a641a53"
+                "sha256:1174dcb84d08887b55defb2cda1986faeeea715fff189ef3dc44cce99f5fca6b",
+                "sha256:2613fab506bd2aedb3722c8c64c17f8f74f4070afed6eea17f20b2115e445aec",
+                "sha256:44b82bc1146a24e5b9853d04c142576b4e8fa7a92f2e30bc364a85d1f75c4de2",
+                "sha256:457fcbee4df737d2defc181b9073758d73f54a6cfc1f280533ff48831b39f4a8",
+                "sha256:49603e1a6e24104961497ad0c07c799aec1caac7400a6762b687e74c8206677d",
+                "sha256:8c2f40b99a8153893793559919a355d7b74649a11e59f411b0b0a1793e160bc0",
+                "sha256:e1d897889c3b5a829426b7d52828fb37b28bc181cd598624e65c8be40ee3f7fa"
             ],
-            "version": "==6.0.1"
+            "version": "==6.0.2"
         },
         "traitlets": {
             "hashes": [
diff --git a/docs/img/tutorial/async-sql-databases/image01.png b/docs/img/tutorial/async-sql-databases/image01.png
new file mode 100644 (file)
index 0000000..01dc369
Binary files /dev/null and b/docs/img/tutorial/async-sql-databases/image01.png differ
diff --git a/docs/src/async_sql_databases/tutorial001.py b/docs/src/async_sql_databases/tutorial001.py
new file mode 100644 (file)
index 0000000..cbf43d7
--- /dev/null
@@ -0,0 +1,65 @@
+from typing import List
+
+import databases
+import sqlalchemy
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+# SQLAlchemy specific code, as with any other app
+DATABASE_URL = "sqlite:///./test.db"
+# DATABASE_URL = "postgresql://user:password@postgresserver/db"
+
+database = databases.Database(DATABASE_URL)
+
+metadata = sqlalchemy.MetaData()
+
+notes = sqlalchemy.Table(
+    "notes",
+    metadata,
+    sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
+    sqlalchemy.Column("text", sqlalchemy.String),
+    sqlalchemy.Column("completed", sqlalchemy.Boolean),
+)
+
+
+engine = sqlalchemy.create_engine(
+    DATABASE_URL, connect_args={"check_same_thread": False}
+)
+metadata.create_all(engine)
+
+
+class NoteIn(BaseModel):
+    text: str
+    completed: bool
+
+
+class Note(BaseModel):
+    id: int
+    text: str
+    completed: bool
+
+
+app = FastAPI()
+
+
+@app.on_event("startup")
+async def startup():
+    await database.connect()
+
+
+@app.on_event("shutdown")
+async def shutdown():
+    await database.disconnect()
+
+
+@app.get("/notes/", response_model=List[Note])
+async def read_notes():
+    query = notes.select()
+    return await database.fetch_all(query)
+
+
+@app.post("/notes/", response_model=Note)
+async def create_note(note: NoteIn):
+    query = notes.insert().values(text=note.text, completed=note.completed)
+    last_record_id = await database.execute(query)
+    return {**note.dict(), "id": last_record_id}
diff --git a/docs/tutorial/async-sql-databases.md b/docs/tutorial/async-sql-databases.md
new file mode 100644 (file)
index 0000000..4da6404
--- /dev/null
@@ -0,0 +1,160 @@
+You can also use <a href="https://github.com/encode/databases" target="_blank">`encode/databases`</a> with **FastAPI** to connect to databases using `async` and `await`.
+
+It is compatible with:
+
+* PostgreSQL
+* MySQL
+* SQLite
+
+In this example, we'll use **SQLite**, because it uses a single file and Python has integrated support. So, you can copy this example and run it as is.
+
+Later, for your production application, you might want to use a database server like **PostgreSQL**.
+
+!!! tip
+    You could adopt ideas from the previous section about <a href="/tutorial/sql-databases/" target="_blank">SQLAlchemy ORM</a>, like using utility functions to perform operations in the database, independent of your **FastAPI** code.
+
+    This section doesn't apply those ideas, to be equivalent to the counterpart in <a href="https://www.starlette.io/database/" target="_blank">Starlette</a>.
+
+## Import and set up `SQLAlchemy`
+
+* Import `SQLAlchemy`.
+* Create a `metadata` object.
+* Create a table `notes` using the `metadata` object.
+
+```Python hl_lines="4 14 16 17 18 19 20 21 22"
+{!./src/async_sql_databases/tutorial001.py!}
+```
+
+!!! tip
+    Notice that all this code is pure SQLAlchemy Core.
+
+    `databases` is not doing anything here yet.
+
+## Import and set up `databases`
+
+* Import `databases`.
+* Create a `DATABASE_URL`.
+* Create a `database` object.
+
+```Python hl_lines="3 9 12"
+{!./src/async_sql_databases/tutorial001.py!}
+```
+
+!!! tip
+    If you where connecting to a different database (e.g. PostgreSQL), you would need to change the `DATABASE_URL`.
+
+## Create the tables
+
+In this case, we are creating the tables in the same Python file, but in production, you would probably want to create them with Alembic, integrated with migrations, etc.
+
+Here, this section would run directly, right before starting your **FastAPI** application.
+
+* Create an `engine`.
+* Create all the tables from the `metadata` object.
+
+```Python hl_lines="25 26 27 28"
+{!./src/async_sql_databases/tutorial001.py!}
+```
+
+## Create models
+
+Create Pydantic models for:
+
+* Notes to be created (`NoteIn`).
+* Notes to be returned (`Note`).
+
+```Python hl_lines="31 32 33 36 37 38 39"
+{!./src/async_sql_databases/tutorial001.py!}
+```
+
+By creating these Pydantic models, the input data will be validated, serialized (converted), and annotated (documented).
+
+So, you will be able to see it all in the interactive API docs.
+
+## Connect and disconnect
+
+* Create your `FastAPI` application.
+* Create event handlers to connect and disconnect from the database.
+
+```Python hl_lines="42 45 46 47 50 51 52"
+{!./src/async_sql_databases/tutorial001.py!}
+```
+
+## Read notes
+
+Create the *path operation function* to read notes:
+
+```Python hl_lines="55 56 57 58"
+{!./src/async_sql_databases/tutorial001.py!}
+```
+
+!!! Note
+    Notice that as we communicate with the database using `await`, the *path operation function* is declared with `async`.
+
+### Notice the `response_model=List[Note]`
+
+It uses `typing.List`.
+
+That documents (and validates, serializes, filters) the output data, as a `list` of `Note`s.
+
+## Create notes
+
+Create the *path operation function* to create notes:
+
+```Python hl_lines="61 62 63 64 65"
+{!./src/async_sql_databases/tutorial001.py!}
+```
+
+!!! Note
+    Notice that as we communicate with the database using `await`, the *path operation function* is declared with `async`.
+
+### About `{**note.dict(), "id": last_record_id}`
+
+`note` is a Pydantic `Note` object.
+
+`note.dict()` returns a `dict` with its data, something like:
+
+```Python
+{
+    "text": "Some note",
+    "completed": False,
+}
+```
+
+but it doesn't have the `id` field.
+
+So we create a new `dict`, that contains the key-value pairs from `note.dict()` with:
+
+```Python
+{**note.dict()}
+```
+
+`**note.dict()` "unpacks" the key value pairs directly, so, `{**note.dict()}` would be, more or less, a copy of `note.dict()`.
+
+And then, we extend that copy `dict`, adding another key-value pair: `"id": last_record_id`:
+
+```Python
+{**note.dict(), "id": last_record_id}
+```
+
+So, the final result returned would be something like:
+
+```Python
+{
+    "id": 1,
+    "text": "Some note",
+    "completed": False,
+}
+```
+
+## Check it
+
+You can copy this code as is, and see the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
+
+There you can see all your API documented and interact with it:
+
+<img src="/img/tutorial/async-sql-databases/image01.png">
+
+## More info
+
+You can read more about <a href="https://github.com/encode/databases" target="_blank">`encode/databases` at its GitHub page</a>.
index 1eed39a8c9467b36711f4b417817d706fd87d9cc..f550d486784063f82e913637f19f9817a3f2d0aa 100644 (file)
@@ -57,6 +57,7 @@ nav:
             - OAuth2 with Password (and hashing), Bearer with JWT tokens: 'tutorial/security/oauth2-jwt.md'
         - Using the Request Directly: 'tutorial/using-request-directly.md'    
         - SQL (Relational) Databases: 'tutorial/sql-databases.md'
+        - Async SQL (Relational) Databases: 'tutorial/async-sql-databases.md'
         - NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
         - Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
         - Background Tasks: 'tutorial/background-tasks.md'
index 54c28c22f28dcc2e0fa91565e23a47c4cb7e8ec1..232009c8958babb9df4a423660d4b9ce86af22dc 100644 (file)
@@ -37,7 +37,8 @@ test = [
     "isort",
     "requests",
     "email_validator",
-    "sqlalchemy"
+    "sqlalchemy",
+    "databases[sqlite]",
 ]
 doc = [
     "mkdocs",
diff --git a/tests/test_tutorial/test_async_sql_databases/__init__.py b/tests/test_tutorial/test_async_sql_databases/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py
new file mode 100644 (file)
index 0000000..0293a6d
--- /dev/null
@@ -0,0 +1,131 @@
+from starlette.testclient import TestClient
+
+from async_sql_databases.tutorial001 import app
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "Fast API", "version": "0.1.0"},
+    "paths": {
+        "/notes/": {
+            "get": {
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "title": "Response_Read_Notes",
+                                    "type": "array",
+                                    "items": {"$ref": "#/components/schemas/Note"},
+                                }
+                            }
+                        },
+                    }
+                },
+                "summary": "Read Notes Get",
+                "operationId": "read_notes_notes__get",
+            },
+            "post": {
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Note"}
+                            }
+                        },
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+                "summary": "Create Note Post",
+                "operationId": "create_note_notes__post",
+                "requestBody": {
+                    "content": {
+                        "application/json": {
+                            "schema": {"$ref": "#/components/schemas/NoteIn"}
+                        }
+                    },
+                    "required": True,
+                },
+            },
+        }
+    },
+    "components": {
+        "schemas": {
+            "NoteIn": {
+                "title": "NoteIn",
+                "required": ["text", "completed"],
+                "type": "object",
+                "properties": {
+                    "text": {"title": "Text", "type": "string"},
+                    "completed": {"title": "Completed", "type": "boolean"},
+                },
+            },
+            "Note": {
+                "title": "Note",
+                "required": ["id", "text", "completed"],
+                "type": "object",
+                "properties": {
+                    "id": {"title": "Id", "type": "integer"},
+                    "text": {"title": "Text", "type": "string"},
+                    "completed": {"title": "Completed", "type": "boolean"},
+                },
+            },
+            "ValidationError": {
+                "title": "ValidationError",
+                "required": ["loc", "msg", "type"],
+                "type": "object",
+                "properties": {
+                    "loc": {
+                        "title": "Location",
+                        "type": "array",
+                        "items": {"type": "string"},
+                    },
+                    "msg": {"title": "Message", "type": "string"},
+                    "type": {"title": "Error Type", "type": "string"},
+                },
+            },
+            "HTTPValidationError": {
+                "title": "HTTPValidationError",
+                "type": "object",
+                "properties": {
+                    "detail": {
+                        "title": "Detail",
+                        "type": "array",
+                        "items": {"$ref": "#/components/schemas/ValidationError"},
+                    }
+                },
+            },
+        }
+    },
+}
+
+
+def test_openapi_schema():
+    with TestClient(app) as client:
+        response = client.get("/openapi.json")
+        assert response.status_code == 200
+        assert response.json() == openapi_schema
+
+
+def test_create_read():
+    with TestClient(app) as client:
+        note = {"text": "Foo bar", "completed": False}
+        response = client.post("/notes/", json=note)
+        assert response.status_code == 200
+        data = response.json()
+        assert data["text"] == note["text"]
+        assert data["completed"] == note["completed"]
+        assert "id" in data
+        response = client.get(f"/notes/")
+        assert response.status_code == 200
+        assert data in response.json()