ujson = "*"
flake8 = "*"
python-multipart = "*"
+sqlalchemy = "*"
[packages]
starlette = "==0.10.1"
{
"_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": [
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)
)
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
* 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.
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!}
```
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
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!}
```
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!}
```
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!}
```
## 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!}
```
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
+}
+```
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)
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:
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,
+ )
"black",
"isort",
"requests",
- "email_validator"
+ "email_validator",
+ "sqlalchemy"
]
doc = [
"mkdocs",
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
--- /dev/null
+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,
+ }