Compare commits

..

7 Commits
0.2.1 ... 0.4.0

Author SHA1 Message Date
Sebastián Ramírez
acbcbba94f 🔖 Release 0.4.0 with openapi_prefix, #26 2019-02-14 23:04:55 +04:00
Sebastián Ramírez
f7b7a099c3 📝 Update Release Notes and openapi_prefix docs 2019-02-14 23:02:47 +04:00
Kabir Khan
0ea0d0e82a Add Open API prefix route - correct docs behind reverse proxy (#26)
Add Open API prefix route - correct docs behind reverse proxy.
2019-02-14 22:57:49 +04:00
Sebastián Ramírez
890f1f7899 📝 Add note about DB Browser for SQLite in SQL docs 2019-02-12 23:31:18 +04:00
Sebastián Ramírez
783816a7e3 📝 Update Release Notes 2019-02-12 23:07:54 +04:00
Sebastián Ramírez
7863490c8c 🔖 Release after SQLAlchemy fix: 0.3.0 2019-02-12 23:06:05 +04:00
Sebastián Ramírez
955e9fcb31 Update fix SQLAlchemy support with ORM (#30)
 SQLAlchemy ORM support

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

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

*  Add sqlalchemy to testing dependencies (not required to use FastAPI)
2019-02-12 23:02:21 +04:00
22 changed files with 550 additions and 114 deletions

View File

@@ -21,6 +21,7 @@ email-validator = "*"
ujson = "*"
flake8 = "*"
python-multipart = "*"
sqlalchemy = "*"
[packages]
starlette = "==0.10.1"

108
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "20483e725e92e679c4c21ea3ff0043d759c74102b181f16b67908f979f854d5c"
"sha256": "37b34bb892b6b4dc0f7c941434d0e08199aa7a7ca83efb6294b89ace44168bba"
},
"pipfile-spec": 6,
"requires": {
@@ -50,10 +50,10 @@
},
"atomicwrites": {
"hashes": [
"sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0",
"sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
],
"version": "==1.2.1"
"version": "==1.3.0"
},
"attrs": {
"hashes": [
@@ -199,19 +199,19 @@
},
"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": [
@@ -395,19 +395,18 @@
},
"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": [
@@ -418,10 +417,10 @@
},
"nbconvert": {
"hashes": [
"sha256:08d21cf4203fabafd0d09bbd63f06131b411db8ebeede34b0fd4be4548351779",
"sha256:a8a2749f972592aa9250db975304af6b7337f32337e523a2c995cc9e12c07807"
"sha256:302554a2e219bc0fc84f3edd3e79953f3767b46ab67626fdec16e38ba3f7efe4",
"sha256:5de8fb2284422272a1d45abc77c07b888127550a6d602ce619592a2b08a474ff"
],
"version": "==5.4.0"
"version": "==5.4.1"
},
"nbformat": {
"hashes": [
@@ -445,10 +444,10 @@
},
"parso": {
"hashes": [
"sha256:4b8f9ed80c3a4a3191aa3261505d868aa552dd25649cb13a7d73b6b7315edf2d",
"sha256:5a120be2e8863993b597f1c0437efca799e90e0793c98ae5d4e34ebd00140e31"
"sha256:6ecf7244be8e7283ec9009c72d074830e7e0e611c974f813d76db0390a4e0dd6",
"sha256:8162be7570ffb34ec0b8d215d7f3b6c5fab24f51eb3886d6dee362de96b6db94"
],
"version": "==0.3.2"
"version": "==0.3.3"
},
"pexpect": {
"hashes": [
@@ -531,9 +530,9 @@
},
"pyrsistent": {
"hashes": [
"sha256:5a3827d57ad3e46820e5ee4ed5b9e0ee7bc4686df6634a7368bc1863a5c48a77"
"sha256:07f7ae71291af8b0dbad8c2ab630d8223e4a8c4e10fc37badda158c02e753acf"
],
"version": "==0.14.9"
"version": "==0.14.10"
},
"pytest": {
"hashes": [
@@ -553,10 +552,10 @@
},
"python-dateutil": {
"hashes": [
"sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
"sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
],
"version": "==2.7.5"
"version": "==2.8.0"
},
"python-multipart": {
"hashes": [
@@ -640,6 +639,13 @@
],
"version": "==1.12.0"
},
"sqlalchemy": {
"hashes": [
"sha256:7dede29f121071da9873e7b8c98091874617858e790dc364ffaab4b09d81216c"
],
"index": "pypi",
"version": "==1.3.0b3"
},
"terminado": {
"hashes": [
"sha256:55abf9ade563b8f9be1f34e4233c7b7bde726059947a593322e8a553cc4c067a",
@@ -663,9 +669,9 @@
},
"tornado": {
"hashes": [
"sha256:00ebd485a52bd7eaa3f35bdf8ab43c109aaa2edc722849b6905c1ffd8c958e82"
"sha256:d3b719a0cb7094e2b1ca94b31f4b601639fa7ad01a548a1a2ccdd6cbdfd56671"
],
"version": "==6.0a1"
"version": "==6.0b1"
},
"traitlets": {
"hashes": [
@@ -676,29 +682,27 @@
},
"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"
"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.2.0"
"version": "==1.3.1"
},
"ujson": {
"hashes": [

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,6 +1,18 @@
## Next
## 0.4.0
* Add `openapi_prefix`, support for reverse proxy and mounting sub-applicaitons. See the docs at <a href="https://fastapi.tiangolo.com/tutorial/sub-applications-proxy/" target="_blank">https://fastapi.tiangolo.com/tutorial/sub-applications-proxy/</a>: <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank">#26</a> by <a href="https://github.com/kabirkhan" target="_blank">@kabirkhan</a>
* Update <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">docs/tutorial for SQLAlchemy</a> including note about *DB Browser for SQLite*.
## 0.3.0
* Fix/add SQLAlchemy support, including ORM, and update <a href="https://fastapi.tiangolo.com/tutorial/sql-databases/" target="_blank">docs for SQLAlchemy</a>: <a href="https://github.com/tiangolo/fastapi/pull/30" target="_blank">#30</a>
## 0.2.1
* Fix `jsonable_encoder` for Pydantic models with `Config` but without `json_encoders`: <a href="https://github.com/tiangolo/fastapi/pull/29" target="_blank">#29</a>.
* Fix `jsonable_encoder` for Pydantic models with `Config` but without `json_encoders`: <a href="https://github.com/tiangolo/fastapi/pull/29" target="_blank">#29</a>
## 0.2.0

View 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

View File

@@ -0,0 +1,19 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/app")
def read_main():
return {"message": "Hello World from main app"}
subapi = FastAPI(openapi_prefix="/subapi")
@subapi.get("/sub")
def read_sub():
return {"message": "Hello World from sub API"}
app.mount("/subapi", subapi)

View 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,55 @@ 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
}
```
## Interact with the database direclty
If you want to explore the SQLite database (file) directly, independently of FastAPI, to debug its contents, add tables, columns, records, modify data, etc. you can use <a href="https://sqlitebrowser.org/" target="_blank">DB Browser for SQLite</a>.
It will look like this:
<img src="/img/tutorial/sql-databases/image02.png">

View File

@@ -0,0 +1,95 @@
There are at least two situations where you could need to create your **FastAPI** application using some specific paths.
But then you need to set them up to be served with a path prefix.
It could happen if you have a:
* **Proxy** server.
* You are "**mounting**" a FastAPI application inside another FastAPI application (or inside another ASGI application, like Starlette).
## Proxy
Having a proxy in this case means that you could declare a path at `/app`, but then, you could need to add a layer on top (the Proxy) that would put your **FastAPI** application under a path like `/api/v1`.
In this case, the original path `/app` will actually be served at `/api/v1/app`.
Even though your application "thinks" it is serving at `/app`.
And the Proxy could be re-writing the path "on the fly" to keep your application convinced that it is serving at `/app`.
Up to here, everything would work as normally.
But then, when you open the integrated docs, they would expect to get the OpenAPI schema at `/openapi.json`, instead of `/api/v1/openapi.json`.
So, the frontend (that runs in the browser) would try to reach `/openapi.json` and wouldn't be able to get the OpenAPI schema.
So, it's needed that the frontend looks for the OpenAPI schema at `/api/v1/openapi.json`.
And it's also needed that the returned JSON OpenAPI schema has the defined path at `/api/v1/app` (behind the proxy) instead of `/app`.
---
For these cases, you can declare an `openapi_prefix` parameter in your `FastAPI` application.
See the section below, about "mounting", for an example.
## Mounting a **FastAPI** application
"Mounting" means adding a complete "independent" application in a specific path, that then takes care of handling all the sub-paths.
You could want to do this if you have several "independent" applications that you want to separate, having their own independent OpenAPI schema and user interfaces.
### Top-level application
First, create the main, top-level, **FastAPI** application, and its path operations:
```Python hl_lines="3 6 7 8"
{!./src/sub_applications/tutorial001.py!}
```
### Sub-application
Then, create your sub-application, and its path operations.
This sub-application is just another standard FastAPI application, but this is the one that will be "mounted".
When creating the sub-application, use the parameter `openapi_prefix`. In this case, with a prefix of `/subapi`:
```Python hl_lines="11 14 15 16"
{!./src/sub_applications/tutorial001.py!}
```
### Mount the sub-application
In your top-level application, `app`, mount the sub-application, `subapi`.
Here you need to make sure you use the same path that you used for the `openapi_prefix`, in this case, `/subapi`:
```Python hl_lines="11 19"
{!./src/sub_applications/tutorial001.py!}
```
## Check the automatic API docs
Now, run `uvicorn`, if your file is at `main.py`, it would be:
```bash
uvicorn main:app --debug
```
And open the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
You will see the automatic API docs for the main app, including only its own paths:
<img src="/img/tutorial/sub-applications/image01.png">
And then, open the docs for the sub-application, at <a href="http://127.0.0.1:8000/subapi/docs" target="_blank">http://127.0.0.1:8000/subapi/docs</a>.
You will see the automatic API docs for the sub-application, including only its own sub-paths, with their correct prefix:
<img src="/img/tutorial/sub-applications/image02.png">
If you try interacting with any of the two user interfaces, they will work, because the browser will be able to talk to the correct path (or sub-path).

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.2.1"
__version__ = "0.4.0"
from .applications import FastAPI
from .routing import APIRouter

View File

@@ -25,6 +25,7 @@ class FastAPI(Starlette):
description: str = "",
version: str = "0.1.0",
openapi_url: Optional[str] = "/openapi.json",
openapi_prefix: str = "",
docs_url: Optional[str] = "/docs",
redoc_url: Optional[str] = "/redoc",
**extra: Dict[str, Any],
@@ -43,6 +44,7 @@ class FastAPI(Starlette):
self.description = description
self.version = version
self.openapi_url = openapi_url
self.openapi_prefix = openapi_prefix.rstrip("/")
self.docs_url = docs_url
self.redoc_url = redoc_url
self.extra = extra
@@ -66,6 +68,7 @@ class FastAPI(Starlette):
openapi_version=self.openapi_version,
description=self.description,
routes=self.routes,
openapi_prefix=self.openapi_prefix,
)
return self.openapi_schema
@@ -80,7 +83,8 @@ class FastAPI(Starlette):
self.add_route(
self.docs_url,
lambda r: get_swagger_ui_html(
openapi_url=self.openapi_url, title=self.title + " - Swagger UI"
openapi_url=self.openapi_prefix + self.openapi_url,
title=self.title + " - Swagger UI",
),
include_in_schema=False,
)
@@ -88,7 +92,8 @@ class FastAPI(Starlette):
self.add_route(
self.redoc_url,
lambda r: get_redoc_html(
openapi_url=self.openapi_url, title=self.title + " - ReDoc"
openapi_url=self.openapi_prefix + self.openapi_url,
title=self.title + " - ReDoc",
),
include_in_schema=False,
)

View 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,
)

View File

@@ -215,7 +215,8 @@ def get_openapi(
version: str,
openapi_version: str = "3.0.2",
description: str = None,
routes: Sequence[BaseRoute]
routes: Sequence[BaseRoute],
openapi_prefix: str = ""
) -> Dict:
info = {"title": title, "version": version}
if description:
@@ -234,7 +235,7 @@ def get_openapi(
if result:
path, security_schemes, path_definitions = result
if path:
paths.setdefault(route.path, {}).update(path)
paths.setdefault(openapi_prefix + route.path, {}).update(path)
if security_schemes:
components.setdefault("securitySchemes", {}).update(
security_schemes

View File

@@ -57,6 +57,7 @@ nav:
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
- Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
- Sub Applications - Behind a Proxy: 'tutorial/sub-applications-proxy.md'
- Application Configuration: 'tutorial/application-configuration.md'
- Extra Starlette options: 'tutorial/extra-starlette.md'
- Concurrency and async / await: 'async.md'

View File

@@ -36,7 +36,8 @@ test = [
"black",
"isort",
"requests",
"email_validator"
"email_validator",
"sqlalchemy"
]
doc = [
"mkdocs",

View File

@@ -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

View File

View File

@@ -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,
}

View File

View File

@@ -0,0 +1,66 @@
from starlette.testclient import TestClient
from sub_applications.tutorial001 import app
client = TestClient(app)
openapi_schema_main = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/app": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Main Get",
"operationId": "read_main_app_get",
}
}
},
}
openapi_schema_sub = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/subapi/sub": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Sub Get",
"operationId": "read_sub_sub_get",
}
}
},
}
def test_openapi_schema_main():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema_main
def test_main():
response = client.get("/app")
assert response.status_code == 200
assert response.json() == {"message": "Hello World from main app"}
def test_openapi_schema_sub():
response = client.get("/subapi/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema_sub
def test_sub():
response = client.get("/subapi/sub")
assert response.status_code == 200
assert response.json() == {"message": "Hello World from sub API"}