--- /dev/null
+# Update with Extra Data (Hashed Passwords) with FastAPI
+
+In the previous chapter I explained to you how to update data in the database from input data coming from a **FastAPI** *path operation*.
+
+Now I'll explain to you how to add **extra data**, additional to the input data, when updating or creating a model object.
+
+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**.
+
+## Password Hashing
+
+Let's imagine that each hero in our system also has a **password**.
+
+We should never store the password in plain text in the database, we should only stored a **hashed version** of it.
+
+"**Hashing**" means converting some content (a password in this case) into a sequence of bytes (just a string) that looks like gibberish.
+
+Whenever you pass exactly the same content (exactly the same password) you get exactly the same gibberish.
+
+But you **cannot convert** from the gibberish **back to the password**.
+
+### Why use Password Hashing
+
+If your database is stolen, the thief won't have your users' **plaintext passwords**, only the hashes.
+
+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).
+
+/// tip
+
+You could use <a href="https://passlib.readthedocs.io/en/stable/" class="external-link" target="_blank">passlib</a> to hash passwords.
+
+In this example we will use a fake hashing function to focus on the data changes. 🤡
+
+///
+
+## Update Models with Extra Data
+
+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.
+
+```Python hl_lines="11 15 26"
+# Code above omitted 👆
+
+{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:7-30]!}
+
+# Code below omitted 👇
+```
+
+/// details | 👀 Full file preview
+
+```Python
+{!./docs_src/tutorial/fastapi/update/tutorial002.py!}
+```
+
+///
+
+When a client is creating a new hero, they will send the `password` in the request body.
+
+And when they are updating a hero, they could also send the `password` in the request body to update it.
+
+## Hash the Password
+
+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.
+
+```Python hl_lines="11"
+# Code above omitted 👆
+
+{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:44-46]!}
+
+# Code here omitted 👈
+
+{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:57-59]!}
+
+# Code below omitted 👇
+```
+
+/// details | 👀 Full file preview
+
+```Python
+{!./docs_src/tutorial/fastapi/update/tutorial002.py!}
+```
+
+///
+
+## Create an Object with Extra Data
+
+Now we need to create the database hero.
+
+In previous examples, we have used something like:
+
+```Python
+db_hero = Hero.model_validate(hero)
+```
+
+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.
+
+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.
+
+`Hero` actually has a `hashed_password`, but we are not providing it. We need a way to provide it...
+
+### Dictionary Update
+
+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:
+
+```Python hl_lines="14"
+db_user_dict = {
+ "name": "Deadpond",
+ "secret_name": "Dive Wilson",
+ "age": None,
+}
+
+hashed_password = "fakehashedpassword"
+
+extra_data = {
+ "hashed_password": hashed_password,
+ "age": 32,
+}
+
+db_user_dict.update(extra_data)
+
+print(db_user_dict)
+
+# {
+# "name": "Deadpond",
+# "secret_name": "Dive Wilson",
+# "age": 32,
+# "hashed_password": "fakehashedpassword",
+# }
+```
+
+This `update` method allows us to add and override things in the original dictionary with the data from another dictionary.
+
+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**.
+
+### Create a Model Object with Extra Data
+
+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:
+
+```Python hl_lines="8"
+# Code above omitted 👆
+
+{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:57-66]!}
+
+# Code below omitted 👇
+```
+
+/// details | 👀 Full file preview
+
+```Python
+{!./docs_src/tutorial/fastapi/update/tutorial002.py!}
+```
+
+///
+
+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`.
+
+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`.
+
+If there's a field in both `hero` and the `extra_data`, **the value from the `extra_data` passed to `update` will take precedence**.
+
+## Update with Extra Data
+
+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.
+
+```Python hl_lines="9"
+# Code above omitted 👆
+
+{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:85-91]!}
+
+# Code below omitted 👇
+```
+
+/// details | 👀 Full file preview
+
+```Python
+{!./docs_src/tutorial/fastapi/update/tutorial002.py!}
+```
+
+///
+
+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`.
+
+Then we can put that `hashed_password` in a dictionary.
+
+And then we can update the `db_hero` object using the method `db_hero.sqlmodel_update()`.
+
+It takes a model object or dictionary with the data to update the object and also an **additional `update` argument** with extra data.
+
+```Python hl_lines="15"
+# Code above omitted 👆
+
+{!./docs_src/tutorial/fastapi/update/tutorial002.py[ln:85-101]!}
+
+# Code below omitted 👇
+```
+
+/// details | 👀 Full file preview
+
+```Python
+{!./docs_src/tutorial/fastapi/update/tutorial002.py!}
+```
+
+///
+
+/// tip
+
+The method `db_hero.sqlmodel_update()` was added in SQLModel 0.0.16. 😎
+
+///
+
+## Recap
+
+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. 🤓
/// 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.
+///
## Update the Hero in the Database
-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()`.
+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`.
-```Python hl_lines="10-11"
+```Python hl_lines="10"
# Code above omitted 👆
{!./docs_src/tutorial/fastapi/update/tutorial001.py[ln:76-91]!}
///
-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**.
+/// tip
-So, if `key` was `"name"` and `value` was `"Deadpuddle"`, then this code:
+The method `db_hero.sqlmodel_update()` was added in SQLModel 0.0.16. 🤓
-```Python
-setattr(db_hero, key, value)
-```
+Before that, you would need to manually get the values and set them using `setattr()`.
-...would be more or less equivalent to:
+///
-```Python
-db_hero.name = "Deadpuddle"
-```
+The method `db_hero.sqlmodel_update()` takes an argument with another model object or a dictionary.
+
+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.
## Remove Fields
if not db_hero:
raise HTTPException(status_code=404, detail="Hero not found")
hero_data = hero.model_dump(exclude_unset=True)
- for key, value in hero_data.items():
- setattr(db_hero, key, value)
+ db_hero.sqlmodel_update(hero_data)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
if not db_hero:
raise HTTPException(status_code=404, detail="Hero not found")
hero_data = hero.model_dump(exclude_unset=True)
- for key, value in hero_data.items():
- setattr(db_hero, key, value)
+ db_hero.sqlmodel_update(hero_data)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
if not db_hero:
raise HTTPException(status_code=404, detail="Hero not found")
hero_data = hero.model_dump(exclude_unset=True)
- for key, value in hero_data.items():
- setattr(db_hero, key, value)
+ db_hero.sqlmodel_update(hero_data)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
--- /dev/null
+from typing import List, Optional
+
+from fastapi import FastAPI, HTTPException, Query
+from sqlmodel import Field, Session, SQLModel, create_engine, select
+
+
+class HeroBase(SQLModel):
+ name: str = Field(index=True)
+ secret_name: str
+ age: Optional[int] = Field(default=None, index=True)
+
+
+class Hero(HeroBase, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ hashed_password: str = Field()
+
+
+class HeroCreate(HeroBase):
+ password: str
+
+
+class HeroRead(HeroBase):
+ id: int
+
+
+class HeroUpdate(SQLModel):
+ name: Optional[str] = None
+ secret_name: Optional[str] = None
+ age: Optional[int] = None
+ password: Optional[str] = None
+
+
+sqlite_file_name = "database.db"
+sqlite_url = f"sqlite:///{sqlite_file_name}"
+
+connect_args = {"check_same_thread": False}
+engine = create_engine(sqlite_url, echo=True, connect_args=connect_args)
+
+
+def create_db_and_tables():
+ SQLModel.metadata.create_all(engine)
+
+
+def hash_password(password: str) -> str:
+ # Use something like passlib here
+ return f"not really hashed {password} hehehe"
+
+
+app = FastAPI()
+
+
+@app.on_event("startup")
+def on_startup():
+ create_db_and_tables()
+
+
+@app.post("/heroes/", response_model=HeroRead)
+def create_hero(hero: HeroCreate):
+ hashed_password = hash_password(hero.password)
+ with Session(engine) as session:
+ extra_data = {"hashed_password": hashed_password}
+ db_hero = Hero.model_validate(hero, update=extra_data)
+ session.add(db_hero)
+ session.commit()
+ session.refresh(db_hero)
+ return db_hero
+
+
+@app.get("/heroes/", response_model=List[HeroRead])
+def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)):
+ with Session(engine) as session:
+ heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
+ return heroes
+
+
+@app.get("/heroes/{hero_id}", response_model=HeroRead)
+def read_hero(hero_id: int):
+ with Session(engine) as session:
+ hero = session.get(Hero, hero_id)
+ if not hero:
+ raise HTTPException(status_code=404, detail="Hero not found")
+ return hero
+
+
+@app.patch("/heroes/{hero_id}", response_model=HeroRead)
+def update_hero(hero_id: int, hero: HeroUpdate):
+ with Session(engine) as session:
+ db_hero = session.get(Hero, hero_id)
+ if not db_hero:
+ raise HTTPException(status_code=404, detail="Hero not found")
+ hero_data = hero.model_dump(exclude_unset=True)
+ extra_data = {}
+ if "password" in hero_data:
+ password = hero_data["password"]
+ hashed_password = hash_password(password)
+ extra_data["hashed_password"] = hashed_password
+ db_hero.sqlmodel_update(hero_data, update=extra_data)
+ session.add(db_hero)
+ session.commit()
+ session.refresh(db_hero)
+ return db_hero
--- /dev/null
+from fastapi import FastAPI, HTTPException, Query
+from sqlmodel import Field, Session, SQLModel, create_engine, select
+
+
+class HeroBase(SQLModel):
+ name: str = Field(index=True)
+ secret_name: str
+ age: int | None = Field(default=None, index=True)
+
+
+class Hero(HeroBase, table=True):
+ id: int | None = Field(default=None, primary_key=True)
+ hashed_password: str = Field()
+
+
+class HeroCreate(HeroBase):
+ password: str
+
+
+class HeroRead(HeroBase):
+ id: int
+
+
+class HeroUpdate(SQLModel):
+ name: str | None = None
+ secret_name: str | None = None
+ age: int | None = None
+ password: str | None = None
+
+
+sqlite_file_name = "database.db"
+sqlite_url = f"sqlite:///{sqlite_file_name}"
+
+connect_args = {"check_same_thread": False}
+engine = create_engine(sqlite_url, echo=True, connect_args=connect_args)
+
+
+def create_db_and_tables():
+ SQLModel.metadata.create_all(engine)
+
+
+def hash_password(password: str) -> str:
+ # Use something like passlib here
+ return f"not really hashed {password} hehehe"
+
+
+app = FastAPI()
+
+
+@app.on_event("startup")
+def on_startup():
+ create_db_and_tables()
+
+
+@app.post("/heroes/", response_model=HeroRead)
+def create_hero(hero: HeroCreate):
+ hashed_password = hash_password(hero.password)
+ with Session(engine) as session:
+ extra_data = {"hashed_password": hashed_password}
+ db_hero = Hero.model_validate(hero, update=extra_data)
+ session.add(db_hero)
+ session.commit()
+ session.refresh(db_hero)
+ return db_hero
+
+
+@app.get("/heroes/", response_model=list[HeroRead])
+def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)):
+ with Session(engine) as session:
+ heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
+ return heroes
+
+
+@app.get("/heroes/{hero_id}", response_model=HeroRead)
+def read_hero(hero_id: int):
+ with Session(engine) as session:
+ hero = session.get(Hero, hero_id)
+ if not hero:
+ raise HTTPException(status_code=404, detail="Hero not found")
+ return hero
+
+
+@app.patch("/heroes/{hero_id}", response_model=HeroRead)
+def update_hero(hero_id: int, hero: HeroUpdate):
+ with Session(engine) as session:
+ db_hero = session.get(Hero, hero_id)
+ if not db_hero:
+ raise HTTPException(status_code=404, detail="Hero not found")
+ hero_data = hero.model_dump(exclude_unset=True)
+ update_data = {}
+ if "password" in hero_data:
+ password = hero_data["password"]
+ hashed_password = hash_password(password)
+ update_data["hashed_password"] = hashed_password
+ db_hero.sqlmodel_update(hero_data, update=update_data)
+ session.add(db_hero)
+ session.commit()
+ session.refresh(db_hero)
+ return db_hero
--- /dev/null
+from typing import Optional
+
+from fastapi import FastAPI, HTTPException, Query
+from sqlmodel import Field, Session, SQLModel, create_engine, select
+
+
+class HeroBase(SQLModel):
+ name: str = Field(index=True)
+ secret_name: str
+ age: Optional[int] = Field(default=None, index=True)
+
+
+class Hero(HeroBase, table=True):
+ id: Optional[int] = Field(default=None, primary_key=True)
+ hashed_password: str = Field()
+
+
+class HeroCreate(HeroBase):
+ password: str
+
+
+class HeroRead(HeroBase):
+ id: int
+
+
+class HeroUpdate(SQLModel):
+ name: Optional[str] = None
+ secret_name: Optional[str] = None
+ age: Optional[int] = None
+ password: Optional[str] = None
+
+
+sqlite_file_name = "database.db"
+sqlite_url = f"sqlite:///{sqlite_file_name}"
+
+connect_args = {"check_same_thread": False}
+engine = create_engine(sqlite_url, echo=True, connect_args=connect_args)
+
+
+def create_db_and_tables():
+ SQLModel.metadata.create_all(engine)
+
+
+def hash_password(password: str) -> str:
+ # Use something like passlib here
+ return f"not really hashed {password} hehehe"
+
+
+app = FastAPI()
+
+
+@app.on_event("startup")
+def on_startup():
+ create_db_and_tables()
+
+
+@app.post("/heroes/", response_model=HeroRead)
+def create_hero(hero: HeroCreate):
+ hashed_password = hash_password(hero.password)
+ with Session(engine) as session:
+ extra_data = {"hashed_password": hashed_password}
+ db_hero = Hero.model_validate(hero, update=extra_data)
+ session.add(db_hero)
+ session.commit()
+ session.refresh(db_hero)
+ return db_hero
+
+
+@app.get("/heroes/", response_model=list[HeroRead])
+def read_heroes(offset: int = 0, limit: int = Query(default=100, le=100)):
+ with Session(engine) as session:
+ heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
+ return heroes
+
+
+@app.get("/heroes/{hero_id}", response_model=HeroRead)
+def read_hero(hero_id: int):
+ with Session(engine) as session:
+ hero = session.get(Hero, hero_id)
+ if not hero:
+ raise HTTPException(status_code=404, detail="Hero not found")
+ return hero
+
+
+@app.patch("/heroes/{hero_id}", response_model=HeroRead)
+def update_hero(hero_id: int, hero: HeroUpdate):
+ with Session(engine) as session:
+ db_hero = session.get(Hero, hero_id)
+ if not db_hero:
+ raise HTTPException(status_code=404, detail="Hero not found")
+ hero_data = hero.model_dump(exclude_unset=True)
+ update_data = {}
+ if "password" in hero_data:
+ password = hero_data["password"]
+ hashed_password = hash_password(password)
+ update_data["hashed_password"] = hashed_password
+ db_hero.sqlmodel_update(hero_data, update=update_data)
+ session.add(db_hero)
+ session.commit()
+ session.refresh(db_hero)
+ return db_hero
- tutorial/fastapi/read-one.md
- tutorial/fastapi/limit-and-offset.md
- tutorial/fastapi/update.md
+ - tutorial/fastapi/update-extra-data.md
- tutorial/fastapi/delete.md
- tutorial/fastapi/session-with-dependency.md
- tutorial/fastapi/teams.md
TYPE_CHECKING,
AbstractSet,
Any,
+ Callable,
Dict,
ForwardRef,
Generator,
)
from pydantic import VERSION as PYDANTIC_VERSION
+from pydantic import BaseModel
from pydantic.fields import FieldInfo
from typing_extensions import get_args, get_origin
update: Dict[str, Any]
def __getattribute__(self, __name: str) -> Any:
- if __name in self.update:
- return self.update[__name]
- return getattr(self.obj, __name)
+ update = super().__getattribute__("update")
+ obj = super().__getattribute__("obj")
+ if __name in update:
+ return update[__name]
+ return getattr(obj, __name)
def _is_union_type(t: Any) -> bool:
) -> None:
model.model_config[parameter] = value # type: ignore[literal-required]
- def get_model_fields(model: InstanceOrType["SQLModel"]) -> Dict[str, "FieldInfo"]:
+ def get_model_fields(model: InstanceOrType[BaseModel]) -> Dict[str, "FieldInfo"]:
return model.model_fields
+ def get_fields_set(
+ object: InstanceOrType["SQLModel"],
+ ) -> Union[Set[str], Callable[[BaseModel], Set[str]]]:
+ return object.model_fields_set
+
def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None:
object.__setattr__(new_object, "__pydantic_fields_set__", set())
object.__setattr__(new_object, "__pydantic_extra__", None)
) -> None:
setattr(model.__config__, parameter, value) # type: ignore
- def get_model_fields(model: InstanceOrType["SQLModel"]) -> Dict[str, "FieldInfo"]:
+ def get_model_fields(model: InstanceOrType[BaseModel]) -> Dict[str, "FieldInfo"]:
return model.__fields__ # type: ignore
+ def get_fields_set(
+ object: InstanceOrType["SQLModel"],
+ ) -> Union[Set[str], Callable[[BaseModel], Set[str]]]:
+ return object.__fields_set__
+
def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None:
object.__setattr__(new_object, "__fields_set__", set())
update=update,
)
- # TODO: remove when deprecating Pydantic v1, only for compatibility
def model_dump(
self,
*,
exclude_unset=exclude_unset,
update=update,
)
+
+ def sqlmodel_update(
+ self: _TSQLModel,
+ obj: Union[Dict[str, Any], BaseModel],
+ *,
+ update: Union[Dict[str, Any], None] = None,
+ ) -> _TSQLModel:
+ use_update = (update or {}).copy()
+ if isinstance(obj, dict):
+ for key, value in {**obj, **use_update}.items():
+ if key in get_model_fields(self):
+ setattr(self, key, value)
+ elif isinstance(obj, BaseModel):
+ for key in get_model_fields(obj):
+ if key in use_update:
+ value = use_update.pop(key)
+ else:
+ value = getattr(obj, key)
+ setattr(self, key, value)
+ for remaining_key in use_update:
+ if remaining_key in get_model_fields(self):
+ value = use_update.pop(remaining_key)
+ setattr(self, remaining_key, value)
+ else:
+ raise ValueError(
+ "Can't use sqlmodel_update() with something that "
+ f"is not a dict or SQLModel or Pydantic model: {obj}"
+ )
+ return self
from datetime import datetime, timedelta
from sqlmodel import Field, SQLModel
+from sqlmodel._compat import get_fields_set
def test_fields_set():
last_updated: datetime = Field(default_factory=datetime.now)
user = User(username="bob")
- assert user.__fields_set__ == {"username"}
+ assert get_fields_set(user) == {"username"}
user = User(username="bob", email="bob@test.com")
- assert user.__fields_set__ == {"username", "email"}
+ assert get_fields_set(user) == {"username", "email"}
user = User(
username="bob",
email="bob@test.com",
last_updated=datetime.now() - timedelta(days=1),
)
- assert user.__fields_set__ == {"username", "email", "last_updated"}
+ assert get_fields_set(user) == {"username", "email", "last_updated"}
--- /dev/null
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+from sqlmodel import Session, create_engine
+from sqlmodel.pool import StaticPool
+
+
+def test_tutorial(clear_sqlmodel):
+ from docs_src.tutorial.fastapi.update import tutorial002 as mod
+
+ mod.sqlite_url = "sqlite://"
+ mod.engine = create_engine(
+ mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
+ )
+
+ with TestClient(mod.app) as client:
+ hero1_data = {
+ "name": "Deadpond",
+ "secret_name": "Dive Wilson",
+ "password": "chimichanga",
+ }
+ hero2_data = {
+ "name": "Spider-Boy",
+ "secret_name": "Pedro Parqueador",
+ "id": 9000,
+ "password": "auntmay",
+ }
+ hero3_data = {
+ "name": "Rusty-Man",
+ "secret_name": "Tommy Sharp",
+ "age": 48,
+ "password": "bestpreventer",
+ }
+ response = client.post("/heroes/", json=hero1_data)
+ assert response.status_code == 200, response.text
+ hero1 = response.json()
+ assert "password" not in hero1
+ assert "hashed_password" not in hero1
+ hero1_id = hero1["id"]
+ response = client.post("/heroes/", json=hero2_data)
+ assert response.status_code == 200, response.text
+ hero2 = response.json()
+ hero2_id = hero2["id"]
+ response = client.post("/heroes/", json=hero3_data)
+ assert response.status_code == 200, response.text
+ hero3 = response.json()
+ hero3_id = hero3["id"]
+ response = client.get(f"/heroes/{hero2_id}")
+ assert response.status_code == 200, response.text
+ fetched_hero2 = response.json()
+ assert "password" not in fetched_hero2
+ assert "hashed_password" not in fetched_hero2
+ response = client.get("/heroes/9000")
+ assert response.status_code == 404, response.text
+ response = client.get("/heroes/")
+ assert response.status_code == 200, response.text
+ data = response.json()
+ assert len(data) == 3
+ for response_hero in data:
+ assert "password" not in response_hero
+ assert "hashed_password" not in response_hero
+
+ # Test hashed passwords
+ with Session(mod.engine) as session:
+ hero1_db = session.get(mod.Hero, hero1_id)
+ assert hero1_db
+ assert not hasattr(hero1_db, "password")
+ assert hero1_db.hashed_password == "not really hashed chimichanga hehehe"
+ hero2_db = session.get(mod.Hero, hero2_id)
+ assert hero2_db
+ assert not hasattr(hero2_db, "password")
+ assert hero2_db.hashed_password == "not really hashed auntmay hehehe"
+ hero3_db = session.get(mod.Hero, hero3_id)
+ assert hero3_db
+ assert not hasattr(hero3_db, "password")
+ assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe"
+
+ response = client.patch(
+ f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"}
+ )
+ data = response.json()
+ assert response.status_code == 200, response.text
+ assert data["name"] == hero2_data["name"], "The name should not be set to none"
+ assert (
+ data["secret_name"] == "Spider-Youngster"
+ ), "The secret name should be updated"
+ assert "password" not in data
+ assert "hashed_password" not in data
+ with Session(mod.engine) as session:
+ hero2b_db = session.get(mod.Hero, hero2_id)
+ assert hero2b_db
+ assert not hasattr(hero2b_db, "password")
+ assert hero2b_db.hashed_password == "not really hashed auntmay hehehe"
+
+ response = client.patch(f"/heroes/{hero3_id}", json={"age": None})
+ data = response.json()
+ assert response.status_code == 200, response.text
+ assert data["name"] == hero3_data["name"]
+ assert (
+ data["age"] is None
+ ), "A field should be updatable to None, even if that's the default"
+ assert "password" not in data
+ assert "hashed_password" not in data
+ with Session(mod.engine) as session:
+ hero3b_db = session.get(mod.Hero, hero3_id)
+ assert hero3b_db
+ assert not hasattr(hero3b_db, "password")
+ assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe"
+
+ # Test update dict, hashed_password
+ response = client.patch(
+ f"/heroes/{hero3_id}", json={"password": "philantroplayboy"}
+ )
+ data = response.json()
+ assert response.status_code == 200, response.text
+ assert data["name"] == hero3_data["name"]
+ assert data["age"] is None
+ assert "password" not in data
+ assert "hashed_password" not in data
+ with Session(mod.engine) as session:
+ hero3b_db = session.get(mod.Hero, hero3_id)
+ assert hero3b_db
+ assert not hasattr(hero3b_db, "password")
+ assert (
+ hero3b_db.hashed_password == "not really hashed philantroplayboy hehehe"
+ )
+
+ response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"})
+ assert response.status_code == 404, response.text
+
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/heroes/": {
+ "get": {
+ "summary": "Read Heroes",
+ "operationId": "read_heroes_heroes__get",
+ "parameters": [
+ {
+ "required": False,
+ "schema": {
+ "title": "Offset",
+ "type": "integer",
+ "default": 0,
+ },
+ "name": "offset",
+ "in": "query",
+ },
+ {
+ "required": False,
+ "schema": {
+ "title": "Limit",
+ "maximum": 100,
+ "type": "integer",
+ "default": 100,
+ },
+ "name": "limit",
+ "in": "query",
+ },
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Read Heroes Heroes Get",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/HeroRead"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ "post": {
+ "summary": "Create Hero",
+ "operationId": "create_hero_heroes__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroCreate"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroRead"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ },
+ "/heroes/{hero_id}": {
+ "get": {
+ "summary": "Read Hero",
+ "operationId": "read_hero_heroes__hero_id__get",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "Hero Id", "type": "integer"},
+ "name": "hero_id",
+ "in": "path",
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroRead"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ "patch": {
+ "summary": "Update Hero",
+ "operationId": "update_hero_heroes__hero_id__patch",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "Hero Id", "type": "integer"},
+ "name": "hero_id",
+ "in": "path",
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroUpdate"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroRead"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ },
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ }
+ },
+ },
+ "HeroCreate": {
+ "title": "HeroCreate",
+ "required": ["name", "secret_name", "password"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "secret_name": {"title": "Secret Name", "type": "string"},
+ "age": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Age",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Age", "type": "integer"}
+ ),
+ "password": {"type": "string", "title": "Password"},
+ },
+ },
+ "HeroRead": {
+ "title": "HeroRead",
+ "required": ["name", "secret_name", "id"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "secret_name": {"title": "Secret Name", "type": "string"},
+ "age": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Age",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Age", "type": "integer"}
+ ),
+ "id": {"title": "Id", "type": "integer"},
+ },
+ },
+ "HeroUpdate": {
+ "title": "HeroUpdate",
+ "type": "object",
+ "properties": {
+ "name": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Name",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Name", "type": "string"}
+ ),
+ "secret_name": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Secret Name",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Secret Name", "type": "string"}
+ ),
+ "age": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Age",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Age", "type": "integer"}
+ ),
+ "password": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Password",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Password", "type": "string"}
+ ),
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }
--- /dev/null
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+from sqlmodel import Session, create_engine
+from sqlmodel.pool import StaticPool
+
+from ....conftest import needs_py310
+
+
+@needs_py310
+def test_tutorial(clear_sqlmodel):
+ from docs_src.tutorial.fastapi.update import tutorial002_py310 as mod
+
+ mod.sqlite_url = "sqlite://"
+ mod.engine = create_engine(
+ mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
+ )
+
+ with TestClient(mod.app) as client:
+ hero1_data = {
+ "name": "Deadpond",
+ "secret_name": "Dive Wilson",
+ "password": "chimichanga",
+ }
+ hero2_data = {
+ "name": "Spider-Boy",
+ "secret_name": "Pedro Parqueador",
+ "id": 9000,
+ "password": "auntmay",
+ }
+ hero3_data = {
+ "name": "Rusty-Man",
+ "secret_name": "Tommy Sharp",
+ "age": 48,
+ "password": "bestpreventer",
+ }
+ response = client.post("/heroes/", json=hero1_data)
+ assert response.status_code == 200, response.text
+ hero1 = response.json()
+ assert "password" not in hero1
+ assert "hashed_password" not in hero1
+ hero1_id = hero1["id"]
+ response = client.post("/heroes/", json=hero2_data)
+ assert response.status_code == 200, response.text
+ hero2 = response.json()
+ hero2_id = hero2["id"]
+ response = client.post("/heroes/", json=hero3_data)
+ assert response.status_code == 200, response.text
+ hero3 = response.json()
+ hero3_id = hero3["id"]
+ response = client.get(f"/heroes/{hero2_id}")
+ assert response.status_code == 200, response.text
+ fetched_hero2 = response.json()
+ assert "password" not in fetched_hero2
+ assert "hashed_password" not in fetched_hero2
+ response = client.get("/heroes/9000")
+ assert response.status_code == 404, response.text
+ response = client.get("/heroes/")
+ assert response.status_code == 200, response.text
+ data = response.json()
+ assert len(data) == 3
+ for response_hero in data:
+ assert "password" not in response_hero
+ assert "hashed_password" not in response_hero
+
+ # Test hashed passwords
+ with Session(mod.engine) as session:
+ hero1_db = session.get(mod.Hero, hero1_id)
+ assert hero1_db
+ assert not hasattr(hero1_db, "password")
+ assert hero1_db.hashed_password == "not really hashed chimichanga hehehe"
+ hero2_db = session.get(mod.Hero, hero2_id)
+ assert hero2_db
+ assert not hasattr(hero2_db, "password")
+ assert hero2_db.hashed_password == "not really hashed auntmay hehehe"
+ hero3_db = session.get(mod.Hero, hero3_id)
+ assert hero3_db
+ assert not hasattr(hero3_db, "password")
+ assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe"
+
+ response = client.patch(
+ f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"}
+ )
+ data = response.json()
+ assert response.status_code == 200, response.text
+ assert data["name"] == hero2_data["name"], "The name should not be set to none"
+ assert (
+ data["secret_name"] == "Spider-Youngster"
+ ), "The secret name should be updated"
+ assert "password" not in data
+ assert "hashed_password" not in data
+ with Session(mod.engine) as session:
+ hero2b_db = session.get(mod.Hero, hero2_id)
+ assert hero2b_db
+ assert not hasattr(hero2b_db, "password")
+ assert hero2b_db.hashed_password == "not really hashed auntmay hehehe"
+
+ response = client.patch(f"/heroes/{hero3_id}", json={"age": None})
+ data = response.json()
+ assert response.status_code == 200, response.text
+ assert data["name"] == hero3_data["name"]
+ assert (
+ data["age"] is None
+ ), "A field should be updatable to None, even if that's the default"
+ assert "password" not in data
+ assert "hashed_password" not in data
+ with Session(mod.engine) as session:
+ hero3b_db = session.get(mod.Hero, hero3_id)
+ assert hero3b_db
+ assert not hasattr(hero3b_db, "password")
+ assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe"
+
+ # Test update dict, hashed_password
+ response = client.patch(
+ f"/heroes/{hero3_id}", json={"password": "philantroplayboy"}
+ )
+ data = response.json()
+ assert response.status_code == 200, response.text
+ assert data["name"] == hero3_data["name"]
+ assert data["age"] is None
+ assert "password" not in data
+ assert "hashed_password" not in data
+ with Session(mod.engine) as session:
+ hero3b_db = session.get(mod.Hero, hero3_id)
+ assert hero3b_db
+ assert not hasattr(hero3b_db, "password")
+ assert (
+ hero3b_db.hashed_password == "not really hashed philantroplayboy hehehe"
+ )
+
+ response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"})
+ assert response.status_code == 404, response.text
+
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/heroes/": {
+ "get": {
+ "summary": "Read Heroes",
+ "operationId": "read_heroes_heroes__get",
+ "parameters": [
+ {
+ "required": False,
+ "schema": {
+ "title": "Offset",
+ "type": "integer",
+ "default": 0,
+ },
+ "name": "offset",
+ "in": "query",
+ },
+ {
+ "required": False,
+ "schema": {
+ "title": "Limit",
+ "maximum": 100,
+ "type": "integer",
+ "default": 100,
+ },
+ "name": "limit",
+ "in": "query",
+ },
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Read Heroes Heroes Get",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/HeroRead"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ "post": {
+ "summary": "Create Hero",
+ "operationId": "create_hero_heroes__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroCreate"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroRead"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ },
+ "/heroes/{hero_id}": {
+ "get": {
+ "summary": "Read Hero",
+ "operationId": "read_hero_heroes__hero_id__get",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "Hero Id", "type": "integer"},
+ "name": "hero_id",
+ "in": "path",
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroRead"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ "patch": {
+ "summary": "Update Hero",
+ "operationId": "update_hero_heroes__hero_id__patch",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "Hero Id", "type": "integer"},
+ "name": "hero_id",
+ "in": "path",
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroUpdate"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroRead"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ },
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ }
+ },
+ },
+ "HeroCreate": {
+ "title": "HeroCreate",
+ "required": ["name", "secret_name", "password"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "secret_name": {"title": "Secret Name", "type": "string"},
+ "age": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Age",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Age", "type": "integer"}
+ ),
+ "password": {"type": "string", "title": "Password"},
+ },
+ },
+ "HeroRead": {
+ "title": "HeroRead",
+ "required": ["name", "secret_name", "id"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "secret_name": {"title": "Secret Name", "type": "string"},
+ "age": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Age",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Age", "type": "integer"}
+ ),
+ "id": {"title": "Id", "type": "integer"},
+ },
+ },
+ "HeroUpdate": {
+ "title": "HeroUpdate",
+ "type": "object",
+ "properties": {
+ "name": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Name",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Name", "type": "string"}
+ ),
+ "secret_name": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Secret Name",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Secret Name", "type": "string"}
+ ),
+ "age": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Age",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Age", "type": "integer"}
+ ),
+ "password": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Password",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Password", "type": "string"}
+ ),
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }
--- /dev/null
+from dirty_equals import IsDict
+from fastapi.testclient import TestClient
+from sqlmodel import Session, create_engine
+from sqlmodel.pool import StaticPool
+
+from ....conftest import needs_py39
+
+
+@needs_py39
+def test_tutorial(clear_sqlmodel):
+ from docs_src.tutorial.fastapi.update import tutorial002_py39 as mod
+
+ mod.sqlite_url = "sqlite://"
+ mod.engine = create_engine(
+ mod.sqlite_url, connect_args=mod.connect_args, poolclass=StaticPool
+ )
+
+ with TestClient(mod.app) as client:
+ hero1_data = {
+ "name": "Deadpond",
+ "secret_name": "Dive Wilson",
+ "password": "chimichanga",
+ }
+ hero2_data = {
+ "name": "Spider-Boy",
+ "secret_name": "Pedro Parqueador",
+ "id": 9000,
+ "password": "auntmay",
+ }
+ hero3_data = {
+ "name": "Rusty-Man",
+ "secret_name": "Tommy Sharp",
+ "age": 48,
+ "password": "bestpreventer",
+ }
+ response = client.post("/heroes/", json=hero1_data)
+ assert response.status_code == 200, response.text
+ hero1 = response.json()
+ assert "password" not in hero1
+ assert "hashed_password" not in hero1
+ hero1_id = hero1["id"]
+ response = client.post("/heroes/", json=hero2_data)
+ assert response.status_code == 200, response.text
+ hero2 = response.json()
+ hero2_id = hero2["id"]
+ response = client.post("/heroes/", json=hero3_data)
+ assert response.status_code == 200, response.text
+ hero3 = response.json()
+ hero3_id = hero3["id"]
+ response = client.get(f"/heroes/{hero2_id}")
+ assert response.status_code == 200, response.text
+ fetched_hero2 = response.json()
+ assert "password" not in fetched_hero2
+ assert "hashed_password" not in fetched_hero2
+ response = client.get("/heroes/9000")
+ assert response.status_code == 404, response.text
+ response = client.get("/heroes/")
+ assert response.status_code == 200, response.text
+ data = response.json()
+ assert len(data) == 3
+ for response_hero in data:
+ assert "password" not in response_hero
+ assert "hashed_password" not in response_hero
+
+ # Test hashed passwords
+ with Session(mod.engine) as session:
+ hero1_db = session.get(mod.Hero, hero1_id)
+ assert hero1_db
+ assert not hasattr(hero1_db, "password")
+ assert hero1_db.hashed_password == "not really hashed chimichanga hehehe"
+ hero2_db = session.get(mod.Hero, hero2_id)
+ assert hero2_db
+ assert not hasattr(hero2_db, "password")
+ assert hero2_db.hashed_password == "not really hashed auntmay hehehe"
+ hero3_db = session.get(mod.Hero, hero3_id)
+ assert hero3_db
+ assert not hasattr(hero3_db, "password")
+ assert hero3_db.hashed_password == "not really hashed bestpreventer hehehe"
+
+ response = client.patch(
+ f"/heroes/{hero2_id}", json={"secret_name": "Spider-Youngster"}
+ )
+ data = response.json()
+ assert response.status_code == 200, response.text
+ assert data["name"] == hero2_data["name"], "The name should not be set to none"
+ assert (
+ data["secret_name"] == "Spider-Youngster"
+ ), "The secret name should be updated"
+ assert "password" not in data
+ assert "hashed_password" not in data
+ with Session(mod.engine) as session:
+ hero2b_db = session.get(mod.Hero, hero2_id)
+ assert hero2b_db
+ assert not hasattr(hero2b_db, "password")
+ assert hero2b_db.hashed_password == "not really hashed auntmay hehehe"
+
+ response = client.patch(f"/heroes/{hero3_id}", json={"age": None})
+ data = response.json()
+ assert response.status_code == 200, response.text
+ assert data["name"] == hero3_data["name"]
+ assert (
+ data["age"] is None
+ ), "A field should be updatable to None, even if that's the default"
+ assert "password" not in data
+ assert "hashed_password" not in data
+ with Session(mod.engine) as session:
+ hero3b_db = session.get(mod.Hero, hero3_id)
+ assert hero3b_db
+ assert not hasattr(hero3b_db, "password")
+ assert hero3b_db.hashed_password == "not really hashed bestpreventer hehehe"
+
+ # Test update dict, hashed_password
+ response = client.patch(
+ f"/heroes/{hero3_id}", json={"password": "philantroplayboy"}
+ )
+ data = response.json()
+ assert response.status_code == 200, response.text
+ assert data["name"] == hero3_data["name"]
+ assert data["age"] is None
+ assert "password" not in data
+ assert "hashed_password" not in data
+ with Session(mod.engine) as session:
+ hero3b_db = session.get(mod.Hero, hero3_id)
+ assert hero3b_db
+ assert not hasattr(hero3b_db, "password")
+ assert (
+ hero3b_db.hashed_password == "not really hashed philantroplayboy hehehe"
+ )
+
+ response = client.patch("/heroes/9001", json={"name": "Dragon Cube X"})
+ assert response.status_code == 404, response.text
+
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/heroes/": {
+ "get": {
+ "summary": "Read Heroes",
+ "operationId": "read_heroes_heroes__get",
+ "parameters": [
+ {
+ "required": False,
+ "schema": {
+ "title": "Offset",
+ "type": "integer",
+ "default": 0,
+ },
+ "name": "offset",
+ "in": "query",
+ },
+ {
+ "required": False,
+ "schema": {
+ "title": "Limit",
+ "maximum": 100,
+ "type": "integer",
+ "default": 100,
+ },
+ "name": "limit",
+ "in": "query",
+ },
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Read Heroes Heroes Get",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/HeroRead"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ "post": {
+ "summary": "Create Hero",
+ "operationId": "create_hero_heroes__post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroCreate"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroRead"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ },
+ "/heroes/{hero_id}": {
+ "get": {
+ "summary": "Read Hero",
+ "operationId": "read_hero_heroes__hero_id__get",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "Hero Id", "type": "integer"},
+ "name": "hero_id",
+ "in": "path",
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroRead"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ "patch": {
+ "summary": "Update Hero",
+ "operationId": "update_hero_heroes__hero_id__patch",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "Hero Id", "type": "integer"},
+ "name": "hero_id",
+ "in": "path",
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroUpdate"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HeroRead"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ },
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ }
+ },
+ },
+ "HeroCreate": {
+ "title": "HeroCreate",
+ "required": ["name", "secret_name", "password"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "secret_name": {"title": "Secret Name", "type": "string"},
+ "age": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Age",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Age", "type": "integer"}
+ ),
+ "password": {"type": "string", "title": "Password"},
+ },
+ },
+ "HeroRead": {
+ "title": "HeroRead",
+ "required": ["name", "secret_name", "id"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "secret_name": {"title": "Secret Name", "type": "string"},
+ "age": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Age",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Age", "type": "integer"}
+ ),
+ "id": {"title": "Id", "type": "integer"},
+ },
+ },
+ "HeroUpdate": {
+ "title": "HeroUpdate",
+ "type": "object",
+ "properties": {
+ "name": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Name",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Name", "type": "string"}
+ ),
+ "secret_name": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Secret Name",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Secret Name", "type": "string"}
+ ),
+ "age": IsDict(
+ {
+ "anyOf": [{"type": "integer"}, {"type": "null"}],
+ "title": "Age",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Age", "type": "integer"}
+ ),
+ "password": IsDict(
+ {
+ "anyOf": [{"type": "string"}, {"type": "null"}],
+ "title": "Password",
+ }
+ )
+ | IsDict(
+ # TODO: Remove when deprecating Pydantic v1
+ {"title": "Password", "type": "string"}
+ ),
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {
+ "anyOf": [{"type": "string"}, {"type": "integer"}]
+ },
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }