mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-24 14:48:35 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
783816a7e3 | ||
|
|
7863490c8c | ||
|
|
955e9fcb31 | ||
|
|
9484f939ed | ||
|
|
9745a5d1ae |
1
Pipfile
1
Pipfile
@@ -21,6 +21,7 @@ email-validator = "*"
|
||||
ujson = "*"
|
||||
flake8 = "*"
|
||||
python-multipart = "*"
|
||||
sqlalchemy = "*"
|
||||
|
||||
[packages]
|
||||
starlette = "==0.10.1"
|
||||
|
||||
108
Pipfile.lock
generated
108
Pipfile.lock
generated
@@ -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": [
|
||||
|
||||
BIN
docs/img/tutorial/sql-databases/image01.png
Normal file
BIN
docs/img/tutorial/sql-databases/image01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@@ -1,3 +1,11 @@
|
||||
## 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>
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* Fix typos in Security section: <a href="https://github.com/tiangolo/fastapi/pull/24" target="_blank">#24</a> by <a href="https://github.com/kkinder" target="_blank">@kkinder</a>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.2.0"
|
||||
__version__ = "0.3.0"
|
||||
|
||||
from .applications import FastAPI
|
||||
from .routing import APIRouter
|
||||
|
||||
@@ -13,51 +13,63 @@ def jsonable_encoder(
|
||||
by_alias: bool = False,
|
||||
include_none: bool = True,
|
||||
custom_encoder: dict = {},
|
||||
sqlalchemy_safe: bool = True,
|
||||
) -> Any:
|
||||
if isinstance(obj, BaseModel):
|
||||
if not obj.Config.json_encoders:
|
||||
return jsonable_encoder(
|
||||
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
|
||||
include_none=include_none,
|
||||
)
|
||||
else:
|
||||
return jsonable_encoder(
|
||||
obj.dict(include=include, exclude=exclude, by_alias=by_alias),
|
||||
include_none=include_none,
|
||||
custom_encoder=obj.Config.json_encoders,
|
||||
)
|
||||
encoder = getattr(obj.Config, "json_encoders", custom_encoder)
|
||||
return 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:
|
||||
@@ -76,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,
|
||||
)
|
||||
|
||||
@@ -36,7 +36,8 @@ test = [
|
||||
"black",
|
||||
"isort",
|
||||
"requests",
|
||||
"email_validator"
|
||||
"email_validator",
|
||||
"sqlalchemy"
|
||||
]
|
||||
doc = [
|
||||
"mkdocs",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import FastAPI
|
||||
@@ -18,7 +17,7 @@ class ModelWithDatetimeField(BaseModel):
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
model = ModelWithDatetimeField(dt_field=datetime.utcnow())
|
||||
model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8))
|
||||
|
||||
|
||||
@app.get("/model", response_model=ModelWithDatetimeField)
|
||||
@@ -32,4 +31,4 @@ client = TestClient(app)
|
||||
def test_dt():
|
||||
with client:
|
||||
response = client.get("/model")
|
||||
assert json.loads(model.json()) == response.json()
|
||||
assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
@@ -1,5 +1,9 @@
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
|
||||
import pytest
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Person:
|
||||
@@ -32,6 +36,29 @@ class Unserializable:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ModelWithCustomEncoder(BaseModel):
|
||||
dt_field: datetime
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda dt: dt.replace(
|
||||
microsecond=0, tzinfo=timezone.utc
|
||||
).isoformat()
|
||||
}
|
||||
|
||||
|
||||
class RoleEnum(Enum):
|
||||
admin = "admin"
|
||||
normal = "normal"
|
||||
|
||||
|
||||
class ModelWithConfig(BaseModel):
|
||||
role: RoleEnum = None
|
||||
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
def test_encode_class():
|
||||
person = Person(name="Foo")
|
||||
pet = Pet(owner=person, name="Firulais")
|
||||
@@ -48,3 +75,13 @@ def test_encode_unsupported():
|
||||
unserializable = Unserializable()
|
||||
with pytest.raises(ValueError):
|
||||
jsonable_encoder(unserializable)
|
||||
|
||||
|
||||
def test_encode_custom_json_encoders_model():
|
||||
model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8))
|
||||
assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"}
|
||||
|
||||
|
||||
def test_encode_model_with_config():
|
||||
model = ModelWithConfig(role=RoleEnum.admin)
|
||||
assert jsonable_encoder(model) == {"role": "admin"}
|
||||
|
||||
0
tests/test_tutorial/test_sql_databases/__init__.py
Normal file
0
tests/test_tutorial/test_sql_databases/__init__.py
Normal file
88
tests/test_tutorial/test_sql_databases/test_tutorial001.py
Normal file
88
tests/test_tutorial/test_sql_databases/test_tutorial001.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user