Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tutorial/fastapi/delete.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ We get a `hero_id` from the path parameter and verify if it exists, just as we d

And if we actually find a hero, we just delete it with the **session**.

{* ./docs_src/tutorial/fastapi/delete/tutorial001.py ln[89:97] hl[89:97] *}
{* ./docs_src/tutorial/fastapi/delete/tutorial001.py ln[91:98] hl[91:98] *}

After deleting it successfully, we just return a response of:

Expand Down
6 changes: 4 additions & 2 deletions docs/tutorial/fastapi/limit-and-offset.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ By default, we will return the first results from the database, so `offset` will

And by default, we will return a maximum of `100` heroes, so `limit` will have a default value of `100`.

{* ./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py ln[1:2,52:56] hl[1,53,55] *}
We import `Annotated` from `typing` and `Query` from `fastapi`:

{* ./docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py ln[1:4,54:58] hl[1,3,55,57] *}

We want to allow clients to set different `offset` and `limit` values.

But we don't want them to be able to set a `limit` of something like `9999`, that's over `9000`! 😱

So, to prevent it, we add additional validation to the `limit` query parameter, declaring that it has to be **l**ess than or **e**qual to `100` with `le=100`.
So, to prevent it, we add `Query(le=100)` inside `Annotated`, declaring that the `limit` query parameter has to be **l**ess than or **e**qual to `100`. The `= 100` sets its default value.

This way, a client can decide to take fewer heroes if they want, but not more.

Expand Down
8 changes: 4 additions & 4 deletions docs/tutorial/fastapi/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ It's because we declared the `HeroPublic` with only the same base fields of the

And the same way, we declared the `TeamPublic` with only the same base fields of the `TeamBase` plus the `id`. But it doesn't include a field `heroes` for the **relationship attribute**.

{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[5:7,20:21,29:34,43:44] hl[5:7,20:21,29:34,43:44] *}
{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[7:9,22:23,31:36,45:46] hl[7:9,22:23,31:36,45:46] *}

Now, remember that [FastAPI uses the `response_model` to validate and **filter** the response data](https://fastapi.tiangolo.com/tutorial/response-model/)?

In this case, we used `response_model=TeamPublic` and `response_model=HeroPublic`, so FastAPI will use them to filter the response data, even if we return a **table model** that includes **relationship attributes**:

{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[102:107,155:160] hl[102,107,155,160] *}
{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[103:108,155:160] hl[103,108,155,160] *}

## Don't Include All the Data

Expand Down Expand Up @@ -132,7 +132,7 @@ Let's add the models `HeroPublicWithTeam` and `TeamPublicWithHeroes`.

We'll add them **after** the other models so that we can easily reference the previous models.

{* ./docs_src/tutorial/fastapi/relationships/tutorial001.py ln[59:64] hl[59:60,63:64] *}
{* ./docs_src/tutorial/fastapi/relationships/tutorial001.py ln[61:66] hl[61:62,65:66] *}

These two models are very **simple in code**, but there's a lot happening here. Let's check it out.

Expand Down Expand Up @@ -166,7 +166,7 @@ This will tell **FastAPI** to take the object that we return from the *path oper

In the case of the hero, this tells FastAPI to extract the `team` too. And in the case of the team, to extract the list of `heroes` too.

{* ./docs_src/tutorial/fastapi/relationships/tutorial001.py ln[111:116,164:169] hl[111,116,164,169] *}
{* ./docs_src/tutorial/fastapi/relationships/tutorial001.py ln[112:117,164:169] hl[112,117,164,169] *}

## Check It Out in the Docs UI

Expand Down
28 changes: 9 additions & 19 deletions docs/tutorial/fastapi/session-with-dependency.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Before we keep adding things, let's change a bit how we get the session for each

Up to now, we have been creating a session in each *path operation*, in a `with` block.

{* ./docs_src/tutorial/fastapi/delete/tutorial001.py ln[48:55] hl[50] *}
{* ./docs_src/tutorial/fastapi/delete/tutorial001.py ln[50:57] hl[52] *}

That's perfectly fine, but in many use cases we would want to use [FastAPI Dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/), for example to **verify** that the client is **logged in** and get the **current user** before executing any other code in the *path operation*.

Expand All @@ -20,27 +20,17 @@ A **FastAPI** dependency is very simple, it's just a function that returns a val

It could use `yield` instead of `return`, and in that case **FastAPI** will make sure it executes all the code **after** the `yield`, once it is done with the request.

{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py ln[40:42] hl[40:42] *}
{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py ln[42:44] hl[42:44] *}

## Use the Dependency

Now let's make FastAPI execute a dependency and get its value in the *path operation*.

We import `Depends()` from `fastapi`. Then we use it in the *path operation function* in a **parameter**, the same way we declared parameters to get JSON bodies, path parameters, etc.
We import `Annotated` from `typing` and `Depends` from `fastapi`. Then we declare the `session` parameter as `Annotated[Session, Depends(get_session)]`.

{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py ln[1:2,40:42,53:59] hl[1,54] *}
The type of the parameter is `Session`, and `Depends(get_session)` tells FastAPI to use `get_session` to provide its value.

/// tip

Here's a tip about that `*,` thing in the parameters.

Here we are passing the parameter `session` that has a "default value" of `Depends(get_session)` before the parameter `hero`, that doesn't have any default value.

Python would normally complain about that, but we can use the initial "parameter" `*,` to mark all the rest of the parameters as "keyword only", which solves the problem.

You can read more about it in the FastAPI documentation [Path Parameters and Numeric Validations - Order the parameters as you need, tricks](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#order-the-parameters-as-you-need-tricks)

///
{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py ln[1:4,42:44,55:61] hl[1,3,56] *}

The value of a dependency will **only be used for one request**, FastAPI will call it right before calling your code and will give you the value from that dependency.

Expand All @@ -56,13 +46,13 @@ And because dependencies can use `yield`, FastAPI will make sure to run the code

This means that in the main code of the *path operation function*, it will work equivalently to the previous version with the explicit `with` block.

{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py ln[1:2,40:42,53:59] hl[55:59] *}
{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py ln[1:4,42:44,55:61] hl[57:61] *}

In fact, you could think that all that block of code inside of the `create_hero()` function is still inside a `with` block for the **session**, because this is more or less what's happening behind the scenes.

But now, the `with` block is not explicitly in the function, but in the dependency above:

{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py ln[1:2,40:42,53:59] hl[41:42] *}
{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py ln[1:4,42:44,55:61] hl[43:44] *}

We will see how this is very useful when testing the code later. ✅

Expand All @@ -73,12 +63,12 @@ Now we can update the rest of the *path operations* to use the new dependency.
We just declare the dependency in the parameters of the function, with:

```Python
session: Session = Depends(get_session)
session: Annotated[Session, Depends(get_session)]
```

And then we remove the previous `with` block with the old **session**.

{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py ln[1:2,40:42,53:104] hl[54,65,74,83,98] *}
{* ./docs_src/tutorial/fastapi/session_with_dependency/tutorial001.py ln[1:4,42:44,55:104] hl[56,66,75,84,98] *}

## Recap

Expand Down
8 changes: 4 additions & 4 deletions docs/tutorial/fastapi/teams.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ Then we also inherit from the `TeamBase` for the `TeamCreate` and `TeamPublic` *

And we also create a `TeamUpdate` **data model**.

{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[1:26] hl[5:7,10:13,16:17,20:21,24:26] *}
{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[1:28] hl[7:9,12:15,18:19,22:23,26:28] *}

We now also have **relationship attributes**. 🎉

Let's now update the `Hero` models too.

## Update Hero Models

{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[29:55] hl[29:34,37:40,43:44,47:48,51:55] *}
{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[31:57] hl[31:36,39:42,45:46,49:50,53:57] *}

We now have a `team_id` in the hero models.

Expand All @@ -38,15 +38,15 @@ And even though the `HeroBase` is *not* a **table model**, we can declare `team_

Notice that the **relationship attributes**, the ones with `Relationship()`, are **only** in the **table models**, as those are the ones that are handled by **SQLModel** with SQLAlchemy and that can have the automatic fetching of data from the database when we access them.

{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[5:55] hl[13,40] *}
{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[7:57] hl[15,42] *}

## Path Operations for Teams

Let's now add the **path operations** for teams.

These are equivalent and very similar to the **path operations** for the **heroes** we had before, so we don't have to go over the details for each one, let's check the code.

{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[135:188] hl[135:141,144:152,155:160,163:178,181:188] *}
{* ./docs_src/tutorial/fastapi/teams/tutorial001.py ln[136:187] hl[136:142,145:152,155:160,163:177,180:187] *}

## Using Relationship Attributes

Expand Down
10 changes: 5 additions & 5 deletions docs/tutorial/fastapi/update-extra-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The `Hero` table model will now store a new field `hashed_password`.

And the data models for `HeroCreate` and `HeroUpdate` will also have a new field `password` that will contain the plain text password sent by clients.

{* ./docs_src/tutorial/fastapi/update/tutorial002.py ln[5:28] hl[13,17,28] *}
{* ./docs_src/tutorial/fastapi/update/tutorial002.py ln[7:30] hl[15,19,30] *}

When a client is creating a new hero, they will send the `password` in the request body.

Expand All @@ -50,7 +50,7 @@ The app will receive the data from the client using the `HeroCreate` model.

This contains the `password` field with the plain text password, and we cannot use that one. So we need to generate a hash from it.

{* ./docs_src/tutorial/fastapi/update/tutorial002.py ln[42:44,55:57] hl[57] *}
{* ./docs_src/tutorial/fastapi/update/tutorial002.py ln[44:46,57:59] hl[59] *}

## Create an Object with Extra Data

Expand Down Expand Up @@ -106,7 +106,7 @@ So now, `db_user_dict` has the updated `age` field with `32` instead of `None` a

Similar to how dictionaries have an `update` method, **SQLModel** models have a parameter `update` in `Hero.model_validate()` that takes a dictionary with extra data, or data that should take precedence:

{* ./docs_src/tutorial/fastapi/update/tutorial002.py ln[55:64] hl[60] *}
{* ./docs_src/tutorial/fastapi/update/tutorial002.py ln[57:66] hl[62] *}

Now, `db_hero` (which is a *table model* `Hero`) will extract its values from `hero` (which is a *data model* `HeroCreate`), and then it will **`update`** its values with the extra data from the dictionary `extra_data`.

Expand All @@ -120,7 +120,7 @@ Now let's say we want to **update a hero** that already exists in the database.

The same way as before, to avoid removing existing data, we will use `exclude_unset=True` when calling `hero.model_dump()`, to get a dictionary with only the data sent by the client.

{* ./docs_src/tutorial/fastapi/update/tutorial002.py ln[83:89] hl[89] *}
{* ./docs_src/tutorial/fastapi/update/tutorial002.py ln[85:91] hl[91] *}

Now, this `hero_data` dictionary could contain a `password`. We need to check it, and if it's there, we need to generate the `hashed_password`.

Expand All @@ -130,7 +130,7 @@ And then we can update the `db_hero` object using the method `db_hero.sqlmodel_u

It takes a model object or dictionary with the data to update the object and also an **additional `update` argument** with extra data.

{* ./docs_src/tutorial/fastapi/update/tutorial002.py ln[83:99] hl[95] *}
{* ./docs_src/tutorial/fastapi/update/tutorial002.py ln[85:101] hl[97] *}

/// tip

Expand Down
10 changes: 5 additions & 5 deletions docs/tutorial/fastapi/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Because each field is **actually different** (we just set a default value of `No

So, let's create this new `HeroUpdate` model:

{* ./docs_src/tutorial/fastapi/update/tutorial001.py ln[5:26] hl[23:26] *}
{* ./docs_src/tutorial/fastapi/update/tutorial001.py ln[7:28] hl[25:28] *}

This is almost the same as `HeroBase`, but all the fields are optional, so we can't simply inherit from `HeroBase`.

Expand All @@ -32,7 +32,7 @@ Now let's use this model in the *path operation* to update a hero.

We will use a `PATCH` HTTP operation. This is used to **partially update data**, which is what we are doing.

{* ./docs_src/tutorial/fastapi/update/tutorial001.py ln[74:89] hl[74:75] *}
{* ./docs_src/tutorial/fastapi/update/tutorial001.py ln[76:87] hl[76:77] *}

We also read the `hero_id` from the *path parameter* and the request body, a `HeroUpdate`.

Expand All @@ -42,7 +42,7 @@ We take a `hero_id` with the **ID** of the hero **we want to update**.

So, we need to read the hero from the database, with the **same logic** we used to **read a single hero**, checking if it exists, possibly raising an error for the client if it doesn't exist, etc.

{* ./docs_src/tutorial/fastapi/update/tutorial001.py ln[74:89] hl[77:79] *}
{* ./docs_src/tutorial/fastapi/update/tutorial001.py ln[76:87] hl[79:81] *}

### Get the New Data

Expand Down Expand Up @@ -92,7 +92,7 @@ Then the dictionary we would get in Python using `hero.model_dump(exclude_unset=

Then we use that to get the data that was actually sent by the client:

{* ./docs_src/tutorial/fastapi/update/tutorial001.py ln[74:89] hl[80] *}
{* ./docs_src/tutorial/fastapi/update/tutorial001.py ln[76:87] hl[82] *}

/// tip
Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, but it was renamed to `hero.model_dump(exclude_unset=True)` to be consistent with Pydantic v2.
Expand All @@ -102,7 +102,7 @@ Before SQLModel 0.0.14, the method was called `hero.dict(exclude_unset=True)`, b

Now that we have a **dictionary with the data sent by the client**, we can use the method `db_hero.sqlmodel_update()` to update the object `db_hero`.

{* ./docs_src/tutorial/fastapi/update/tutorial001.py ln[74:89] hl[81] *}
{* ./docs_src/tutorial/fastapi/update/tutorial001.py ln[76:87] hl[83] *}

/// tip

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
So, here we are telling the FastAPI app to use `get_session_override` instead of `get_session` in all the places in the code that depend on `get_session`, that is, all the parameters with something like:

```Python
session: Session = Depends(get_session)
session: Annotated[Session, Depends(get_session)]
```

5. After we are done with the dependency override, we can restore the application back to normal, by removing all the values in this dictionary `app.dependency_overrides`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
5. Now, in the test function, to tell **pytest** that this test wants to get the fixture, instead of declaring something like in FastAPI with:

```Python
session: Session = Depends(session_fixture)
session: Annotated[Session, Depends(session_fixture)]
```

...the way we tell pytest what is the fixture that we want is by using the **exact same name** of the fixture.
Expand Down
15 changes: 8 additions & 7 deletions docs_src/tutorial/fastapi/app_testing/tutorial001/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select

Expand Down Expand Up @@ -51,7 +53,7 @@ def on_startup():


@app.post("/heroes/", response_model=HeroPublic)
def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):
def create_hero(session: Annotated[Session, Depends(get_session)], hero: HeroCreate):
db_hero = Hero.model_validate(hero)
session.add(db_hero)
session.commit()
Expand All @@ -61,17 +63,16 @@ def create_hero(*, session: Session = Depends(get_session), hero: HeroCreate):

@app.get("/heroes/", response_model=list[HeroPublic])
def read_heroes(
*,
session: Session = Depends(get_session),
session: Annotated[Session, Depends(get_session)],
offset: int = 0,
limit: int = Query(default=100, le=100),
limit: Annotated[int, Query(le=100)] = 100,
):
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes


@app.get("/heroes/{hero_id}", response_model=HeroPublic)
def read_hero(*, session: Session = Depends(get_session), hero_id: int):
def read_hero(session: Annotated[Session, Depends(get_session)], hero_id: int):
hero = session.get(Hero, hero_id)
if not hero:
raise HTTPException(status_code=404, detail="Hero not found")
Expand All @@ -80,7 +81,7 @@ def read_hero(*, session: Session = Depends(get_session), hero_id: int):

@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(
*, session: Session = Depends(get_session), hero_id: int, hero: HeroUpdate
session: Annotated[Session, Depends(get_session)], hero_id: int, hero: HeroUpdate
):
db_hero = session.get(Hero, hero_id)
if not db_hero:
Expand All @@ -94,7 +95,7 @@ def update_hero(


@app.delete("/heroes/{hero_id}")
def delete_hero(*, session: Session = Depends(get_session), hero_id: int):
def delete_hero(session: Annotated[Session, Depends(get_session)], hero_id: int):
hero = session.get(Hero, hero_id)
if not hero:
raise HTTPException(status_code=404, detail="Hero not found")
Expand Down
4 changes: 3 additions & 1 deletion docs_src/tutorial/fastapi/delete/tutorial001.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Annotated

from fastapi import FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select

Expand Down Expand Up @@ -56,7 +58,7 @@ def create_hero(hero: HeroCreate):


@app.get("/heroes/", response_model=list[HeroPublic])
def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)):
def read_heroes(offset: int = 0, limit: Annotated[int, Query(le=100)] = 100):
with Session(engine) as session:
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes
Expand Down
4 changes: 3 additions & 1 deletion docs_src/tutorial/fastapi/limit_and_offset/tutorial001.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Annotated

from fastapi import FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select

Expand Down Expand Up @@ -50,7 +52,7 @@ def create_hero(hero: HeroCreate):


@app.get("/heroes/", response_model=list[HeroPublic])
def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)):
def read_heroes(offset: int = 0, limit: Annotated[int, Query(le=100)] = 100):
with Session(engine) as session:
heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
return heroes
Expand Down
Loading
Loading