Skip to content

Commit 6525725

Browse files
authored
Merge branch 'main' into main
2 parents 6f688f5 + 6b56235 commit 6525725

18 files changed

+1897
-40
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ jobs:
6262
run: python -m poetry install
6363
- name: Install Pydantic v1
6464
if: matrix.pydantic-version == 'pydantic-v1'
65-
run: pip install "pydantic>=1.10.0,<2.0.0"
65+
run: pip install --upgrade "pydantic>=1.10.0,<2.0.0"
6666
- name: Install Pydantic v2
6767
if: matrix.pydantic-version == 'pydantic-v2'
68-
run: pip install "pydantic>=2.0.2,<3.0.0"
68+
run: pip install --upgrade "pydantic>=2.0.2,<3.0.0"
6969
- name: Lint
7070
# Do not run on Python 3.7 as mypy behaves differently
7171
if: matrix.python-version != '3.7' && matrix.pydantic-version == 'pydantic-v2'

docs/release-notes.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
## Latest Changes
44

5+
## 0.0.16
6+
7+
### Features
8+
9+
* ✨ Add new method `.sqlmodel_update()` to update models in place, including an `update` parameter for extra data. And fix implementation for the (now documented) `update` parameter for `.model_validate()`. PR [#804](https://github.com/tiangolo/sqlmodel/pull/804) by [@tiangolo](https://github.com/tiangolo).
10+
* Updated docs: [Update Data with FastAPI](https://sqlmodel.tiangolo.com/tutorial/fastapi/update/).
11+
* New docs: [Update with Extra Data (Hashed Passwords) with FastAPI](https://sqlmodel.tiangolo.com/tutorial/fastapi/update-extra-data/).
12+
13+
## 0.0.15
14+
15+
### Fixes
16+
17+
* 🐛 Fix class initialization compatibility with Pydantic and SQLModel, fixing errors revealed by the latest Pydantic. PR [#807](https://github.com/tiangolo/sqlmodel/pull/807) by [@tiangolo](https://github.com/tiangolo).
18+
519
### Internal
620

721
* ⬆ Bump tiangolo/issue-manager from 0.4.0 to 0.4.1. PR [#775](https://github.com/tiangolo/sqlmodel/pull/775) by [@dependabot[bot]](https://github.com/apps/dependabot).
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# Update with Extra Data (Hashed Passwords) with FastAPI
2+
3+
In the previous chapter I explained to you how to update data in the database from input data coming from a **FastAPI** *path operation*.
4+
5+
Now I'll explain to you how to add **extra data**, additional to the input data, when updating or creating a model object.
6+
7+
This is particularly useful when you need to **generate some data** in your code that is **not coming from the client**, but you need to store it in the database. For example, to store a **hashed password**.
8+
9+
## Password Hashing
10+
11+
Let's imagine that each hero in our system also has a **password**.
12+
13+
We should never store the password in plain text in the database, we should only stored a **hashed version** of it.
14+
15+
"**Hashing**" means converting some content (a password in this case) into a sequence of bytes (just a string) that looks like gibberish.
16+
17+
Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish.
18+
19+
But you **cannot convert** from the gibberish **back to the password**.
20+
21+
### Why use Password Hashing
22+
23+
If your database is stolen, the thief won't have your users' **plaintext passwords**, only the hashes.
24+
25+
So, the thief won't be able to try to use that password in another system (as many users use the same password everywhere, this would be dangerous).
26+
27+
/// tip
28+
29+
You could use <a href="https://passlib.readthedocs.io/en/stable/" class="external-link" target="_blank">passlib</a> to hash passwords.
30+
31+
In this example we will use a fake hashing function to focus on the data changes. 🤡
32+
33+
///
34+
35+
## Update Models with Extra Data
36+
37+
The `Hero` table model will now store a new field `hashed_password`.
38+
39+
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.
40+
41+
```Python hl_lines="11 15 26"
42+
# Code above omitted 👆
43+
44+
{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:7-30]!}
45+
46+
# Code below omitted 👇
47+
```
48+
49+
/// details | 👀 Full file preview
50+
51+
```Python
52+
{!./docs_src/tutorial/fastapi/update/tutorial002.py!}
53+
```
54+
55+
///
56+
57+
When a client is creating a new hero, they will send the `password` in the request body.
58+
59+
And when they are updating a hero, they could also send the `password` in the request body to update it.
60+
61+
## Hash the Password
62+
63+
The app will receive the data from the client using the `HeroCreate` model.
64+
65+
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.
66+
67+
```Python hl_lines="11"
68+
# Code above omitted 👆
69+
70+
{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:44-46]!}
71+
72+
# Code here omitted 👈
73+
74+
{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:57-59]!}
75+
76+
# Code below omitted 👇
77+
```
78+
79+
/// details | 👀 Full file preview
80+
81+
```Python
82+
{!./docs_src/tutorial/fastapi/update/tutorial002.py!}
83+
```
84+
85+
///
86+
87+
## Create an Object with Extra Data
88+
89+
Now we need to create the database hero.
90+
91+
In previous examples, we have used something like:
92+
93+
```Python
94+
db_hero = Hero.model_validate(hero)
95+
```
96+
97+
This creates a `Hero` (which is a *table model*) object from the `HeroCreate` (which is a *data model*) object that we received in the request.
98+
99+
And this is all good... but as `Hero` doesn't have a field `password`, it won't be extracted from the object `HeroCreate` that has it.
100+
101+
`Hero` actually has a `hashed_password`, but we are not providing it. We need a way to provide it...
102+
103+
### Dictionary Update
104+
105+
Let's pause for a second to check this, when working with dictionaries, there's a way to `update` a dictionary with extra data from another dictionary, something like this:
106+
107+
```Python hl_lines="14"
108+
db_user_dict = {
109+
"name": "Deadpond",
110+
"secret_name": "Dive Wilson",
111+
"age": None,
112+
}
113+
114+
hashed_password = "fakehashedpassword"
115+
116+
extra_data = {
117+
"hashed_password": hashed_password,
118+
"age": 32,
119+
}
120+
121+
db_user_dict.update(extra_data)
122+
123+
print(db_user_dict)
124+
125+
# {
126+
# "name": "Deadpond",
127+
# "secret_name": "Dive Wilson",
128+
# "age": 32,
129+
# "hashed_password": "fakehashedpassword",
130+
# }
131+
```
132+
133+
This `update` method allows us to add and override things in the original dictionary with the data from another dictionary.
134+
135+
So now, `db_user_dict` has the updated `age` field with `32` instead of `None` and more importantly, **it has the new `hashed_password` field**.
136+
137+
### Create a Model Object with Extra Data
138+
139+
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:
140+
141+
```Python hl_lines="8"
142+
# Code above omitted 👆
143+
144+
{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:57-66]!}
145+
146+
# Code below omitted 👇
147+
```
148+
149+
/// details | 👀 Full file preview
150+
151+
```Python
152+
{!./docs_src/tutorial/fastapi/update/tutorial002.py!}
153+
```
154+
155+
///
156+
157+
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`.
158+
159+
It will only take the fields defined in `Hero`, so **it will not take the `password`** from `HeroCreate`. And it will also **take its values** from the **dictionary passed to the `update`** parameter, in this case, the `hashed_password`.
160+
161+
If there's a field in both `hero` and the `extra_data`, **the value from the `extra_data` passed to `update` will take precedence**.
162+
163+
## Update with Extra Data
164+
165+
Now let's say we want to **update a hero** that already exists in the database.
166+
167+
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.
168+
169+
```Python hl_lines="9"
170+
# Code above omitted 👆
171+
172+
{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:85-91]!}
173+
174+
# Code below omitted 👇
175+
```
176+
177+
/// details | 👀 Full file preview
178+
179+
```Python
180+
{!./docs_src/tutorial/fastapi/update/tutorial002.py!}
181+
```
182+
183+
///
184+
185+
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`.
186+
187+
Then we can put that `hashed_password` in a dictionary.
188+
189+
And then we can update the `db_hero` object using the method `db_hero.sqlmodel_update()`.
190+
191+
It takes a model object or dictionary with the data to update the object and also an **additional `update` argument** with extra data.
192+
193+
```Python hl_lines="15"
194+
# Code above omitted 👆
195+
196+
{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:85-101]!}
197+
198+
# Code below omitted 👇
199+
```
200+
201+
/// details | 👀 Full file preview
202+
203+
```Python
204+
{!./docs_src/tutorial/fastapi/update/tutorial002.py!}
205+
```
206+
207+
///
208+
209+
/// tip
210+
211+
The method `db_hero.sqlmodel_update()` was added in SQLModel 0.0.16. 😎
212+
213+
///
214+
215+
## Recap
216+
217+
You can use the `update` parameter in `Hero.model_validate()` to provide extra data when creating a new object and `Hero.sqlmodel_update()` to provide extra data when updating an existing object. 🤓

docs/tutorial/fastapi/update.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,13 @@ Then we use that to get the data that was actually sent by the client:
154154

155155
/// tip
156156
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.
157+
///
157158

158159
## Update the Hero in the Database
159160

160-
Now that we have a **dictionary with the data sent by the client**, we can iterate for each one of the keys and the values, and then we set them in the database hero model `db_hero` using `setattr()`.
161+
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`.
161162

162-
```Python hl_lines="10-11"
163+
```Python hl_lines="10"
163164
# Code above omitted 👆
164165

165166
{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!}
@@ -175,19 +176,17 @@ Now that we have a **dictionary with the data sent by the client**, we can itera
175176

176177
///
177178

178-
If you are not familiar with that `setattr()`, it takes an object, like the `db_hero`, then an attribute name (`key`), that in our case could be `"name"`, and a value (`value`). And then it **sets the attribute with that name to the value**.
179+
/// tip
179180

180-
So, if `key` was `"name"` and `value` was `"Deadpuddle"`, then this code:
181+
The method `db_hero.sqlmodel_update()` was added in SQLModel 0.0.16. 🤓
181182

182-
```Python
183-
setattr(db_hero, key, value)
184-
```
183+
Before that, you would need to manually get the values and set them using `setattr()`.
185184

186-
...would be more or less equivalent to:
185+
///
187186

188-
```Python
189-
db_hero.name = "Deadpuddle"
190-
```
187+
The method `db_hero.sqlmodel_update()` takes an argument with another model object or a dictionary.
188+
189+
For each of the fields in the **original** model object (`db_hero` in this example), it checks if the field is available in the **argument** (`hero_data` in this example) and then updates it with the provided value.
191190

192191
## Remove Fields
193192

docs_src/tutorial/fastapi/update/tutorial001.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
8080
if not db_hero:
8181
raise HTTPException(status_code=404, detail="Hero not found")
8282
hero_data = hero.model_dump(exclude_unset=True)
83-
for key, value in hero_data.items():
84-
setattr(db_hero, key, value)
83+
db_hero.sqlmodel_update(hero_data)
8584
session.add(db_hero)
8685
session.commit()
8786
session.refresh(db_hero)

docs_src/tutorial/fastapi/update/tutorial001_py310.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
7878
if not db_hero:
7979
raise HTTPException(status_code=404, detail="Hero not found")
8080
hero_data = hero.model_dump(exclude_unset=True)
81-
for key, value in hero_data.items():
82-
setattr(db_hero, key, value)
81+
db_hero.sqlmodel_update(hero_data)
8382
session.add(db_hero)
8483
session.commit()
8584
session.refresh(db_hero)

docs_src/tutorial/fastapi/update/tutorial001_py39.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ def update_hero(hero_id: int, hero: HeroUpdate):
8080
if not db_hero:
8181
raise HTTPException(status_code=404, detail="Hero not found")
8282
hero_data = hero.model_dump(exclude_unset=True)
83-
for key, value in hero_data.items():
84-
setattr(db_hero, key, value)
83+
db_hero.sqlmodel_update(hero_data)
8584
session.add(db_hero)
8685
session.commit()
8786
session.refresh(db_hero)

0 commit comments

Comments
 (0)