]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
Update fix SQLAlchemy support with ORM (#30)
authorSebastián Ramírez <tiangolo@gmail.com>
Tue, 12 Feb 2019 19:02:21 +0000 (23:02 +0400)
committerGitHub <noreply@github.com>
Tue, 12 Feb 2019 19:02:21 +0000 (23:02 +0400)
:sparkles: SQLAlchemy ORM support

Improved jsonable_encoder with SQLAlchemy support, tests running with SQLite, improved and updated SQL docs

* :heavy_plus_sign: Add SQLAlchemy to development dependencies (not required for using FastAPI)

* :heavy_plus_sign: Add sqlalchemy to testing dependencies (not required to use FastAPI)

Pipfile
Pipfile.lock
docs/img/tutorial/sql-databases/image01.png [new file with mode: 0644]
docs/src/sql_databases/tutorial001.py
docs/tutorial/sql-databases.md
fastapi/encoders.py
pyproject.toml
scripts/test.sh
tests/test_tutorial/test_sql_databases/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_sql_databases/test_tutorial001.py [new file with mode: 0644]

diff --git a/Pipfile b/Pipfile
index 5857a08eb0cfd3d5d5a160c3457360f8da8945b0..3a7e2e9ea1990de275a73078c8867c1540a22ad1 100644 (file)
--- a/Pipfile
+++ b/Pipfile
@@ -21,6 +21,7 @@ email-validator = "*"
 ujson = "*"
 flake8 = "*"
 python-multipart = "*"
+sqlalchemy = "*"
 
 [packages]
 starlette = "==0.10.1"
index 26bf2ffe68f8b264b4b00141ca1201320d4945ec..5723c712de618c792f2800bbcac5f361451bc867 100644 (file)
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "20483e725e92e679c4c21ea3ff0043d759c74102b181f16b67908f979f854d5c"
+            "sha256": "37b34bb892b6b4dc0f7c941434d0e08199aa7a7ca83efb6294b89ace44168bba"
         },
         "pipfile-spec": 6,
         "requires": {
         },
         "atomicwrites": {
             "hashes": [
-                "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
-                "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
+                "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
+                "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
             ],
-            "version": "==1.2.1"
+            "version": "==1.3.0"
         },
         "attrs": {
             "hashes": [
         },
         "flake8": {
             "hashes": [
-                "sha256:09b9bb539920776da542e67a570a5df96ff933c9a08b62cfae920bcc789e4383",
-                "sha256:e0f8cd519cfc0072c0ee31add5def09d2b3ef6040b34dc426445c3af9b02163c"
+                "sha256:c3ba1e130c813191db95c431a18cb4d20a468e98af7a77e2181b68574481ad36",
+                "sha256:fd9ddf503110bf3d8b1d270e8c673aab29ccb3dd6abf29bae1f54e5116ab4a91"
             ],
             "index": "pypi",
-            "version": "==3.7.4"
+            "version": "==3.7.5"
         },
         "flit": {
             "hashes": [
-                "sha256:6aefa6ff89a993af7a7af40d3df3d0387d6663df99797981ec41b1431ec6d1e1",
-                "sha256:9969db9708305b64fd8acf20043fcff144f910222397a221fd29871f02ed4a6f"
+                "sha256:1d93f7a833ed8a6e120ddc40db5c4763bc39bccc75c05081ec8285ece718aefb",
+                "sha256:6f6f0fb83c51ffa3a150fa41b5ac118df9ea4a87c2c06dff4ebf9adbe7b52b36"
             ],
             "index": "pypi",
-            "version": "==1.2.1"
+            "version": "==1.3"
         },
         "idna": {
             "hashes": [
         },
         "more-itertools": {
             "hashes": [
-                "sha256:38a936c0a6d98a38bcc2d03fdaaedaba9f412879461dd2ceff8d37564d6522e4",
-                "sha256:c0a5785b1109a6bd7fac76d6837fd1feca158e54e521ccd2ae8bfe393cc9d4fc",
-                "sha256:fe7a7cae1ccb57d33952113ff4fa1bc5f879963600ed74918f1236e212ee50b9"
+                "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
+                "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
             ],
-            "version": "==5.0.0"
+            "version": "==6.0.0"
         },
         "mypy": {
             "hashes": [
-                "sha256:986a7f97808a865405c5fd98fae5ebfa963c31520a56c783df159e9a81e41b3e",
-                "sha256:cc5df73cc11d35655a8c364f45d07b13c8db82c000def4bd7721be13356533b4"
+                "sha256:308c274eb8482fbf16006f549137ddc0d69e5a589465e37b99c4564414363ca7",
+                "sha256:e80fd6af34614a0e898a57f14296d0dacb584648f0339c2e000ddbf0f4cc2f8d"
             ],
             "index": "pypi",
-            "version": "==0.660"
+            "version": "==0.670"
         },
         "mypy-extensions": {
             "hashes": [
         },
         "nbconvert": {
             "hashes": [
-                "sha256:08d21cf4203fabafd0d09bbd63f06131b411db8ebeede34b0fd4be4548351779",
-                "sha256:a8a2749f972592aa9250db975304af6b7337f32337e523a2c995cc9e12c07807"
+                "sha256:302554a2e219bc0fc84f3edd3e79953f3767b46ab67626fdec16e38ba3f7efe4",
+                "sha256:5de8fb2284422272a1d45abc77c07b888127550a6d602ce619592a2b08a474ff"
             ],
-            "version": "==5.4.0"
+            "version": "==5.4.1"
         },
         "nbformat": {
             "hashes": [
         },
         "parso": {
             "hashes": [
-                "sha256:4b8f9ed80c3a4a3191aa3261505d868aa552dd25649cb13a7d73b6b7315edf2d",
-                "sha256:5a120be2e8863993b597f1c0437efca799e90e0793c98ae5d4e34ebd00140e31"
+                "sha256:6ecf7244be8e7283ec9009c72d074830e7e0e611c974f813d76db0390a4e0dd6",
+                "sha256:8162be7570ffb34ec0b8d215d7f3b6c5fab24f51eb3886d6dee362de96b6db94"
             ],
-            "version": "==0.3.2"
+            "version": "==0.3.3"
         },
         "pexpect": {
             "hashes": [
         },
         "pyrsistent": {
             "hashes": [
-                "sha256:5a3827d57ad3e46820e5ee4ed5b9e0ee7bc4686df6634a7368bc1863a5c48a77"
+                "sha256:07f7ae71291af8b0dbad8c2ab630d8223e4a8c4e10fc37badda158c02e753acf"
             ],
-            "version": "==0.14.9"
+            "version": "==0.14.10"
         },
         "pytest": {
             "hashes": [
         },
         "python-dateutil": {
             "hashes": [
-                "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
-                "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
+                "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
+                "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
             ],
-            "version": "==2.7.5"
+            "version": "==2.8.0"
         },
         "python-multipart": {
             "hashes": [
             ],
             "version": "==1.12.0"
         },
+        "sqlalchemy": {
+            "hashes": [
+                "sha256:7dede29f121071da9873e7b8c98091874617858e790dc364ffaab4b09d81216c"
+            ],
+            "index": "pypi",
+            "version": "==1.3.0b3"
+        },
         "terminado": {
             "hashes": [
                 "sha256:55abf9ade563b8f9be1f34e4233c7b7bde726059947a593322e8a553cc4c067a",
         },
         "tornado": {
             "hashes": [
-                "sha256:00ebd485a52bd7eaa3f35bdf8ab43c109aaa2edc722849b6905c1ffd8c958e82"
+                "sha256:d3b719a0cb7094e2b1ca94b31f4b601639fa7ad01a548a1a2ccdd6cbdfd56671"
             ],
-            "version": "==6.0a1"
+            "version": "==6.0b1"
         },
         "traitlets": {
             "hashes": [
         },
         "typed-ast": {
             "hashes": [
-                "sha256:023625bfa9359e29bd6e24cac2a4503495b49761d48a5f1e38333fc4ac4d93fe",
-                "sha256:07591f7a5fdff50e2e566c4c1e9df545c75d21e27d98d18cb405727ed0ef329c",
-                "sha256:153e526b0f4ffbfada72d0bb5ffe8574ba02803d2f3a9c605c8cf99dfedd72a2",
-                "sha256:3ad2bdcd46a4a1518d7376e9f5016d17718a9ed3c6a3f09203d832f6c165de4a",
-                "sha256:3ea98c84df53ada97ee1c5159bb3bc784bd734231235a1ede14c8ae0775049f7",
-                "sha256:51a7141ccd076fa561af107cfb7a8b6d06a008d92451a1ac7e73149d18e9a827",
-                "sha256:52c93cd10e6c24e7ac97e8615da9f224fd75c61770515cb323316c30830ddb33",
-                "sha256:6344c84baeda3d7b33e157f0b292e4dd53d05ddb57a63f738178c01cac4635c9",
-                "sha256:64699ca1b3bd5070bdeb043e6d43bc1d0cebe08008548f4a6bee782b0ecce032",
-                "sha256:74903f2e56bbffe29282ef8a5487d207d10be0f8513b41aff787d954a4cf91c9",
-                "sha256:7891710dba83c29ee2bd51ecaa82f60f6bede40271af781110c08be134207bf2",
-                "sha256:91976c56224e26c256a0de0f76d2004ab885a29423737684b4f7ebdd2f46dde2",
-                "sha256:9bad678a576ecc71f25eba9f1e3fd8d01c28c12a2834850b458428b3e855f062",
-                "sha256:b4726339a4c180a8b6ad9d8b50d2b6dc247e1b79b38fe2290549c98e82e4fd15",
-                "sha256:ba36f6aa3f8933edf94ea35826daf92cbb3ec248b89eccdc053d4a815d285357",
-                "sha256:bbc96bde544fd19e9ef168e4dfa5c3dfe704bfa78128fa76f361d64d6b0f731a",
-                "sha256:c0c927f1e44469056f7f2dada266c79b577da378bbde3f6d2ada726d131e4824",
-                "sha256:c0f9a3708008aa59f560fa1bd22385e05b79b8e38e0721a15a8402b089243442",
-                "sha256:f0bf6f36ff9c5643004171f11d2fdc745aa3953c5aacf2536a0685db9ceb3fb1",
-                "sha256:f5be39a0146be663cbf210a4d95c3c58b2d7df7b043c9047c5448e358f0550a2",
-                "sha256:fcd198bf19d9213e5cbf2cde2b9ef20a9856e716f76f9476157f90ae6de06cc6"
-            ],
-            "version": "==1.2.0"
+                "sha256:035a54ede6ce1380599b2ce57844c6554666522e376bd111eb940fbc7c3dad23",
+                "sha256:037c35f2741ce3a9ac0d55abfcd119133cbd821fffa4461397718287092d9d15",
+                "sha256:049feae7e9f180b64efacbdc36b3af64a00393a47be22fa9cb6794e68d4e73d3",
+                "sha256:19228f7940beafc1ba21a6e8e070e0b0bfd1457902a3a81709762b8b9039b88d",
+                "sha256:2ea681e91e3550a30c2265d2916f40a5f5d89b59469a20f3bad7d07adee0f7a6",
+                "sha256:3a6b0a78af298d82323660df5497bcea0f0a4a25a0b003afd0ce5af049bd1f60",
+                "sha256:5385da8f3b801014504df0852bf83524599df890387a3c2b17b7caa3d78b1773",
+                "sha256:606d8afa07eef77280c2bf84335e24390055b478392e1975f96286d99d0cb424",
+                "sha256:69245b5b23bbf7fb242c9f8f08493e9ecd7711f063259aefffaeb90595d62287",
+                "sha256:6f6d839ab09830d59b7fa8fb6917023d8cb5498ee1f1dbd82d37db78eb76bc99",
+                "sha256:730888475f5ac0e37c1de4bd05eeb799fdb742697867f524dc8a4cd74bcecc23",
+                "sha256:9819b5162ffc121b9e334923c685b0d0826154e41dfe70b2ede2ce29034c71d8",
+                "sha256:9e60ef9426efab601dd9aa120e4ff560f4461cf8442e9c0a2b92548d52800699",
+                "sha256:af5fbdde0690c7da68e841d7fc2632345d570768ea7406a9434446d7b33b0ee1",
+                "sha256:b64efdbdf3bbb1377562c179f167f3bf301251411eb5ac77dec6b7d32bcda463",
+                "sha256:bac5f444c118aeb456fac1b0b5d14c6a71ea2a42069b09c176f75e9bd4c186f6",
+                "sha256:bda9068aafb73859491e13b99b682bd299c1b5fd50644d697533775828a28ee0",
+                "sha256:d659517ca116e6750101a1326107d3479028c5191f0ecee3c7203c50f5b915b0",
+                "sha256:eddd3fb1f3e0f82e5915a899285a39ee34ce18fd25d89582bc89fc9fb16cd2c6"
+            ],
+            "version": "==1.3.1"
         },
         "ujson": {
             "hashes": [
diff --git a/docs/img/tutorial/sql-databases/image01.png b/docs/img/tutorial/sql-databases/image01.png
new file mode 100644 (file)
index 0000000..8e575ab
Binary files /dev/null and b/docs/img/tutorial/sql-databases/image01.png differ
index a847c5c7b9829580cbfa3cc1c25bea7ed9e52133..00eef4c133a4b6d558ccf40182c831c668e029ec 100644 (file)
@@ -1,13 +1,15 @@
 from fastapi import FastAPI
-
 from sqlalchemy import Boolean, Column, Integer, String, create_engine
 from sqlalchemy.ext.declarative import declarative_base, declared_attr
 from sqlalchemy.orm import scoped_session, sessionmaker
 
 # SQLAlchemy specific code, as with any other app
-SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
+SQLALCHEMY_DATABASE_URI = "sqlite:///./test.db"
+# SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
 
-engine = create_engine(SQLALCHEMY_DATABASE_URI, convert_unicode=True)
+engine = create_engine(
+    SQLALCHEMY_DATABASE_URI, connect_args={"check_same_thread": False}
+)
 db_session = scoped_session(
     sessionmaker(autocommit=False, autoflush=False, bind=engine)
 )
@@ -30,15 +32,25 @@ class User(Base):
     is_active = Column(Boolean(), default=True)
 
 
-def get_user(username, db_session):
-    return db_session.query(User).filter(User.id == username).first()
+Base.metadata.create_all(bind=engine)
+
+first_user = db_session.query(User).first()
+if not first_user:
+    u = User(email="johndoe@example.com", hashed_password="notreallyhashed")
+    db_session.add(u)
+    db_session.commit()
+
+
+# Utility
+def get_user(db_session, user_id: int):
+    return db_session.query(User).filter(User.id == user_id).first()
 
 
 # FastAPI specific code
 app = FastAPI()
 
 
-@app.get("/users/{username}")
-def read_user(username: str):
-    user = get_user(username, db_session)
+@app.get("/users/{user_id}")
+def read_user(user_id: int):
+    user = get_user(db_session, user_id=user_id)
     return user
index ef5a2b6c65ee118047c7300b970e6138daafd9bc..1103af48ba7f97251ab9971ed730b70cbe9e620b 100644 (file)
@@ -12,7 +12,9 @@ You can easily adapt it to any database supported by SQLAlchemy, like:
 * Oracle
 * Microsoft SQL Server, etc.
 
-In this example, we'll use **PostgreSQL**.
+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**.
 
 !!! note
     Notice that most of the code is the standard `SQLAlchemy` code you would use with any framework.
@@ -23,30 +25,58 @@ In this example, we'll use **PostgreSQL**.
 
 For now, don't pay attention to the rest, only the imports:
 
-```Python hl_lines="3 4 5"
+```Python hl_lines="2 3 4"
 {!./src/sql_databases/tutorial001.py!}
 ```
 
 ## Define the database
 
-Define the database that SQLAlchemy should connect to:
+Define the database that SQLAlchemy should "connect" to:
 
-```Python hl_lines="8"
+```Python hl_lines="7"
 {!./src/sql_databases/tutorial001.py!}
 ```
 
+In this example, we are "connecting" to a SQLite database (opening a file with the SQLite database).
+
+The file will be located at the same directory in the file `test.db`. That's why the last part is `./test.db`.
+
+If you were using a **PostgreSQL** database instead, you would just have to uncomment the line:
+
+```Python
+SQLALCHEMY_DATABASE_URI = "postgresql://user:password@postgresserver/db"
+```
+
+...and adapt it with your database data and credentials (equivalently for MySQL, MariaDB or any other).
+
 !!! tip
-    This is the main line that you would have to modify if you wanted to use a different database than **PostgreSQL**.
+    
+    This is the main line that you would have to modify if you wanted to use a different database.
 
 ## Create the SQLAlchemy `engine`
 
-```Python hl_lines="10"
+```Python hl_lines="10 11 12"
 {!./src/sql_databases/tutorial001.py!}
 ```
 
+### Note
+
+The argument:
+
+```Python
+connect_args={"check_same_thread": False}
+```
+
+...is needed only for `SQLite`. It's not needed for other databases.
+
+!!! info "Technical Details"
+
+    That argument `check_same_thread` is there mainly to be able to run the tests that cover this example.
+    
+
 ## Create a `scoped_session`
 
-```Python hl_lines="11 12 13"
+```Python hl_lines="13 14 15"
 {!./src/sql_databases/tutorial001.py!}
 ```
 
@@ -55,9 +85,9 @@ Define the database that SQLAlchemy should connect to:
 
     This `scoped_session` is a feature of SQLAlchemy.
 
-    The resulting object, the `db_session` can then be used anywhere a a normal SQLAlchemy session.
+    The resulting object, the `db_session` can then be used anywhere as a normal SQLAlchemy session.
     
-    It can be used as a global because it is implemented to work independently on each "<abbr title="A sequence of code being executed by the program, while at the same time, or at intervals, there can be others being executed too.">thread</abbr>", so the actions you perform with it in one path operation function won't affect the actions performed (possibly concurrently) by other path operation functions.
+    It can be used as a "global" variable because it is implemented to work independently on each "<abbr title="A sequence of code being executed by the program, while at the same time, or at intervals, there can be others being executed too.">thread</abbr>", so the actions you perform with it in one path operation function won't affect the actions performed (possibly concurrently) by other path operation functions.
 
 ## Create a `CustomBase` model
 
@@ -65,17 +95,17 @@ This is more of a trick to facilitate your life than something required.
 
 But by creating this `CustomBase` class and inheriting from it, your models will have automatic `__tablename__` attributes (that are required by SQLAlchemy).
 
-That way you don't have to declare them explicitly.
+That way you don't have to declare them explicitly in every model.
 
 So, your models will behave very similarly to, for example, Flask-SQLAlchemy.
 
-```Python hl_lines="16 17 18 19 20"
+```Python hl_lines="18 19 20 21 22"
 {!./src/sql_databases/tutorial001.py!}
 ```
 
 ## Create the SQLAlchemy `Base` model
 
-```Python hl_lines="23"
+```Python hl_lines="25"
 {!./src/sql_databases/tutorial001.py!}
 ```
 
@@ -85,15 +115,36 @@ Now this is finally code specific to your app.
 
 Here's a user model that will be a table in the database:
 
-```Python hl_lines="26 27 28 29 30"
+```Python hl_lines="28 29 30 31 32"
+{!./src/sql_databases/tutorial001.py!}
+```
+
+## Initialize your application
+
+In a very simplistic way, initialize your database (create the tables, etc) and make sure you have a first user:
+
+```Python hl_lines="35 37 38 39 40 41"
 {!./src/sql_databases/tutorial001.py!}
 ```
 
+### Note
+
+Normally you would probably initialize your database (create tables, etc) with <a href="https://alembic.sqlalchemy.org/en/latest/" target="_blank">Alembic</a>.
+
+And you would also use Alembic for migrations (that's its main job). For whenever you change the structure of your database, add a new column, a new table, etc.
+
+The same way, you would probably make sure there's a first user in an external script that runs before your application, or as part of the application startup.
+
+In this example we are doing those two operations in a very simple way, directly in the code, to focus on the main points.
+
+Also, as all the functionality is self-contained in the same code, you can copy it and run it directly, and it will work as is.
+
+
 ## Get a user
 
-By creating a function that is only dedicated to getting your user from a `username` (or any other parameter) independent of your path operation function, you can more easily re-use it in multiple parts and also add <abbr title="Automated test, written in code, that checks if another piece of code is working correctly.">unit tests</abbr> for it:
+By creating a function that is only dedicated to getting your user from a `user_id` (or any other parameter) independent of your path operation function, you can more easily re-use it in multiple parts and also add <abbr title="Automated tests, written in code, that check if another piece of code is working correctly.">unit tests</abbr> for it:
 
-```Python hl_lines="33 34"
+```Python hl_lines="45 46"
 {!./src/sql_databases/tutorial001.py!}
 ```
 
@@ -103,7 +154,7 @@ Now, finally, here's the standard **FastAPI** code.
 
 Create your app and path operation function:
 
-```Python hl_lines="38 41 42 43 44"
+```Python hl_lines="50 53 54 55 56"
 {!./src/sql_databases/tutorial001.py!}
 ```
 
@@ -113,25 +164,25 @@ We can just call `get_user` directly from inside of the path operation function
 
 ## Create the path operation function
 
-Here we are using SQLAlchemy code inside of the path operation function, and it in turn will go and communicate with an external database. 
+Here we are using SQLAlchemy code inside of the path operation function, and in turn it will go and communicate with an external database. 
 
 That could potentially require some "waiting".
 
-But as SQLAlchemy doesn't have compatibility for using `await`, as would be with something like:
+But as SQLAlchemy doesn't have compatibility for using `await` directly, as would be with something like:
 
 ```Python
-user = await get_user(username, db_session)
+user = await get_user(db_session, user_id=user_id)
 ```
 
 ...and instead we are using:
 
 ```Python
-user = get_user(username, db_session)
+user = get_user(db_session, user_id=user_id)
 ```
 
 Then we should declare the path operation without `async def`, just with a normal `def`:
 
-```Python hl_lines="42"
+```Python hl_lines="54"
 {!./src/sql_databases/tutorial001.py!}
 ```
 
@@ -140,3 +191,47 @@ Then we should declare the path operation without `async def`, just with a norma
 Because we are using SQLAlchemy directly and we don't require any kind of plug-in for it to work with **FastAPI**, we could integrate database <abbr title="Automatically updating the database to have any new column we define in our models.">migrations</abbr> with <a href="https://alembic.sqlalchemy.org" target="_blank">Alembic</a> directly.
 
 You would probably want to declare your database and models in a different file or set of files, this would allow Alembic to import it and use it without even needing to have **FastAPI** installed for the migrations.
+
+## Check it
+
+You can copy this code and use it as is.
+
+!!! info
+
+    In fact, the code shown here is part of the tests. As most of the code in these docs.
+
+
+You can copy it, let's say, to a file `main.py`.
+
+Then you can run it with Uvicorn:
+
+```bash
+uvicorn main:app --debug
+```
+
+And then, you can open your browser at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
+
+And you will be able to interact with your **FastAPI** application, reading data from a real database:
+
+<img src="/img/tutorial/sql-databases/image01.png">
+
+## Response schema and security
+
+This section has the minimum code to show how it works and how you can integrate SQLAlchemy with FastAPI.
+
+But it is recommended that you also create a response model with Pydantic, as described in the section about <a href="/tutorial/extra-models/" target="_blank">Extra Models</a>.
+
+That way you will document the schema of the responses of your API, and you will be able to limit/filter the returned data.
+
+Limiting the returned data is important for security, as for example, you shouldn't be returning the `hashed_password` to the clients.
+
+That's something that you can improve in this example application, here's the current response data:
+
+```JSON
+{
+  "is_active": true,
+  "hashed_password": "notreallyhashed",
+  "email": "johndoe@example.com",
+  "id": 1
+}
+```
index f5059a76f33147cfd76750fc86dd0087e132c062..07e3c3dfb56be24badd782e73a7b1d342dfd55cb 100644 (file)
@@ -13,6 +13,7 @@ def jsonable_encoder(
     by_alias: bool = False,
     include_none: bool = True,
     custom_encoder: dict = {},
+    sqlalchemy_safe: bool = True,
 ) -> Any:
     if isinstance(obj, BaseModel):
         encoder = getattr(obj.Config, "json_encoders", custom_encoder)
@@ -20,39 +21,55 @@ def jsonable_encoder(
             obj.dict(include=include, exclude=exclude, by_alias=by_alias),
             include_none=include_none,
             custom_encoder=encoder,
+            sqlalchemy_safe=sqlalchemy_safe,
         )
     if isinstance(obj, Enum):
         return obj.value
     if isinstance(obj, (str, int, float, type(None))):
         return obj
     if isinstance(obj, dict):
-        return {
-            jsonable_encoder(
-                key,
-                by_alias=by_alias,
-                include_none=include_none,
-                custom_encoder=custom_encoder,
-            ): jsonable_encoder(
-                value,
-                by_alias=by_alias,
-                include_none=include_none,
-                custom_encoder=custom_encoder,
-            )
-            for key, value in obj.items()
-            if value is not None or include_none
-        }
+        encoded_dict = {}
+        for key, value in obj.items():
+            if (
+                (
+                    not sqlalchemy_safe
+                    or (not isinstance(key, str))
+                    or (not key.startswith("_sa"))
+                )
+                and (value is not None or include_none)
+                and ((include and key in include) or key not in exclude)
+            ):
+                encoded_key = jsonable_encoder(
+                    key,
+                    by_alias=by_alias,
+                    include_none=include_none,
+                    custom_encoder=custom_encoder,
+                    sqlalchemy_safe=sqlalchemy_safe,
+                )
+                encoded_value = jsonable_encoder(
+                    value,
+                    by_alias=by_alias,
+                    include_none=include_none,
+                    custom_encoder=custom_encoder,
+                    sqlalchemy_safe=sqlalchemy_safe,
+                )
+                encoded_dict[encoded_key] = encoded_value
+        return encoded_dict
     if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)):
-        return [
-            jsonable_encoder(
-                item,
-                include=include,
-                exclude=exclude,
-                by_alias=by_alias,
-                include_none=include_none,
-                custom_encoder=custom_encoder,
+        encoded_list = []
+        for item in obj:
+            encoded_list.append(
+                jsonable_encoder(
+                    item,
+                    include=include,
+                    exclude=exclude,
+                    by_alias=by_alias,
+                    include_none=include_none,
+                    custom_encoder=custom_encoder,
+                    sqlalchemy_safe=sqlalchemy_safe,
+                )
             )
-            for item in obj
-        ]
+        return encoded_list
     errors = []
     try:
         if custom_encoder and type(obj) in custom_encoder:
@@ -71,4 +88,10 @@ def jsonable_encoder(
             except Exception as e:
                 errors.append(e)
                 raise ValueError(errors)
-    return jsonable_encoder(data, by_alias=by_alias, include_none=include_none)
+    return jsonable_encoder(
+        data,
+        by_alias=by_alias,
+        include_none=include_none,
+        custom_encoder=custom_encoder,
+        sqlalchemy_safe=sqlalchemy_safe,
+    )
index e788fbf53f881d26c81e8dfe709177887485cf8e..c7273cc4b90a4fe4f3ec6fe721f9efd42066fd76 100644 (file)
@@ -36,7 +36,8 @@ test = [
     "black",
     "isort",
     "requests",
-    "email_validator"
+    "email_validator",
+    "sqlalchemy"
 ]
 doc = [
     "mkdocs",
index 79f66c3218c8971ea939b1bb87045fada4c709cf..a9b912bbebbb2f19458ae0d82af2fcfa645c2fee 100755 (executable)
@@ -6,6 +6,11 @@ set -x
 export VERSION_SCRIPT="import sys; print('%s.%s' % sys.version_info[0:2])"
 export PYTHON_VERSION=`python -c "$VERSION_SCRIPT"`
 
+# Remove temporary DB
+if [ -f ./test.db ]; then
+    rm ./test.db
+fi
+
 export PYTHONPATH=./docs/src
 pytest --cov=fastapi --cov=tests --cov=docs/src --cov-report=term-missing ${@}
 mypy fastapi --disallow-untyped-defs
diff --git a/tests/test_tutorial/test_sql_databases/__init__.py b/tests/test_tutorial/test_sql_databases/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_sql_databases/test_tutorial001.py
new file mode 100644 (file)
index 0000000..583c233
--- /dev/null
@@ -0,0 +1,88 @@
+from starlette.testclient import TestClient
+
+from sql_databases.tutorial001 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "Fast API", "version": "0.1.0"},
+    "paths": {
+        "/users/{user_id}": {
+            "get": {
+                "responses": {
+                    "200": {
+                        "description": "Successful Response",
+                        "content": {"application/json": {"schema": {}}},
+                    },
+                    "422": {
+                        "description": "Validation Error",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/HTTPValidationError"
+                                }
+                            }
+                        },
+                    },
+                },
+                "summary": "Read User Get",
+                "operationId": "read_user_users__user_id__get",
+                "parameters": [
+                    {
+                        "required": True,
+                        "schema": {"title": "User_Id", "type": "integer"},
+                        "name": "user_id",
+                        "in": "path",
+                    }
+                ],
+            }
+        }
+    },
+    "components": {
+        "schemas": {
+            "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():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == openapi_schema
+
+
+def test_first_user():
+    response = client.get("/users/1")
+    assert response.status_code == 200
+    assert response.json() == {
+        "is_active": True,
+        "hashed_password": "notreallyhashed",
+        "email": "johndoe@example.com",
+        "id": 1,
+    }