From dc331f649cde36a9755bc1ba6828e4815a4d5517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 24 Aug 2021 14:51:14 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20annotations=20for?= =?UTF-8?q?=20source=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../annotations/en/tutorial002.md | 387 ++++++++++++++++++ .../annotations/en/tutorial003.md | 75 ++++ .../delete/annotations/en/tutorial002.md | 96 +++++ .../annotations/en/test_main_001.md | 17 + .../annotations/en/test_main_002.md | 25 ++ .../annotations/en/test_main_003.md | 37 ++ .../annotations/en/test_main_004.md | 26 ++ .../annotations/en/test_main_005.md | 41 ++ .../annotations/en/test_main_006.md | 23 ++ .../insert/annotations/en/tutorial003.md | 57 +++ .../select/annotations/en/tutorial002.md | 63 +++ .../update/annotations/en/tutorial002.md | 68 +++ .../update/annotations/en/tutorial004.md | 159 +++++++ 13 files changed, 1074 insertions(+) create mode 100644 docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md create mode 100644 docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md create mode 100644 docs_src/tutorial/delete/annotations/en/tutorial002.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_003.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md create mode 100644 docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md create mode 100644 docs_src/tutorial/insert/annotations/en/tutorial003.md create mode 100644 docs_src/tutorial/select/annotations/en/tutorial002.md create mode 100644 docs_src/tutorial/update/annotations/en/tutorial002.md create mode 100644 docs_src/tutorial/update/annotations/en/tutorial004.md diff --git a/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md b/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md new file mode 100644 index 0000000..fd33fec --- /dev/null +++ b/docs_src/tutorial/automatic_id_none_refresh/annotations/en/tutorial002.md @@ -0,0 +1,387 @@ +1. Create the `hero_1`. + + **Doesn't generate any output**. + +2. Create the `hero_2`. + + **Doesn't generate any output**. + +3. Create the `hero_3`. + + **Doesn't generate any output**. + +4. Print the line `"Before interacting with the database"`. + + Generates the output: + + ``` + Before interacting with the database + ``` + +5. Print the `hero_1` before interacting with the database. + + Generates the output: + + ``` + Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None + ``` + +6. Print the `hero_2` before interacting with the database. + + Generates the output: + + ``` + Hero 2: id=None name='Spider-Boy' secret_name='Pedro Parqueador' age=None + ``` + +7. Print the `hero_3` before interacting with the database. + + Generates the output: + + ``` + Hero 3: id=None name='Rusty-Man' secret_name='Tommy Sharp' age=48 + ``` + +8. Create the `Session` in a `with` block. + + **Doesn't generate any output**. + +9. Add the `hero_1` to the session. + + This still doesn't save it to the database. + + **Doesn't generate any output**. + +10. Add the `hero_2` to the session. + + This still doesn't save it to the database. + + **Doesn't generate any output**. + +11. Add the `hero_3` to the session. + + This still doesn't save it to the database. + + **Doesn't generate any output**. + +12. Print the line `"After adding to the session"`. + + Generates the output: + + ``` + After adding to the session + ``` + +13. Print the `hero_1` after adding it to the session. + + It still has the same data as there hasn't been any interaction with the database yet. Notice that the `id` is still `None`. + + Generates the output: + + ``` + Hero 1: id=None name='Deadpond' secret_name='Dive Wilson' age=None + ``` + +14. Print the `hero_2` after adding it to the session. + + It still has the same data as there hasn't been any interaction with the database yet. Notice that the `id` is still `None`. + + Generates the output: + + ``` + Hero 2: id=None name='Spider-Boy' secret_name='Pedro Parqueador' age=None + ``` + +15. Print the `hero_3` after adding it to the session. + + It still has the same data as there hasn't been any interaction with the database yet. Notice that the `id` is still `None`. + + Generates the output: + + ``` + Hero 3: id=None name='Rusty-Man' secret_name='Tommy Sharp' age=48 + ``` + +16. `commit` the **session**. + + This will **save** all the data to the database. The **session** will use the **engine** to run a lot of SQL. + + Generates the output: + + ``` + INFO Engine BEGIN (implicit) + INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) + INFO Engine [generated in 0.00018s] ('Deadpond', 'Dive Wilson', None) + INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) + INFO Engine [cached since 0.0008968s ago] ('Spider-Boy', 'Pedro Parqueador', None) + INFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) + INFO Engine [cached since 0.001143s ago] ('Rusty-Man', 'Tommy Sharp', 48) + INFO Engine COMMIT + ``` + +17. Print the line `"After committing the session"`. + + Generates the output: + + ``` + After committing the session + ``` + +18. Print the `hero_1` after committing the session. + + The `hero_1` is now internally marked as expired, and until it is refreshed, it looks like if it didn't contain any data. + + Generates the output: + + ``` + Hero 1: + ``` + +19. Print the `hero_2` after committing the session. + + The `hero_2` is now internally marked as expired, and until it is refreshed, it looks like if it didn't contain any data. + + Generates the output: + + ``` + Hero 2: + ``` + +20. Print the `hero_3` after committing the session. + + The `hero_3` is now internally marked as expired, and until it is refreshed, it looks like if it didn't contain any data. + + Generates the output: + + ``` + Hero 3: + ``` + +21. Print the line `"After commiting the session, show IDs"`. + + Generates the output: + + ``` + After committing the session, show IDs + ``` + +22. Print the `hero_1.id`. A lot happens here. + + Because we are accessing the attribute `id` of `hero_1`, **SQLModel** (actually SQLAlchemy) can detect that we are trying to access data from the `hero_1`. + + It then detects that `hero_1` is currently associated with a **session** (because we added it to the session and committed it), and it is marked as expired. + + Then with the **session**, it uses the **engine** to execute all the SQL to fetch the data for this object from the database. + + Next it updates the object with the new data and marks it internally as "fresh" or "not expired". + + Finally, it makes the ID value available for the rest of the Python expression. In this case, the Python expression just prints the ID. + + Generates the output: + + ``` + INFO Engine BEGIN (implicit) + INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age + FROM hero + WHERE hero.id = ? + INFO Engine [generated in 0.00017s] (1,) + + Hero 1 ID: 1 + ``` + +23. Print the `hero_2.id`. + + A lot happens here, all the same stuff that happened at point 22, but for this `hero_2` object. + + Generates the output: + + ``` + INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age + FROM hero + WHERE hero.id = ? + INFO Engine [cached since 0.001245s ago] (2,) + + Hero 2 ID: 2 + ``` + +24. Print the `hero_3.id`. + + A lot happens here, all the same stuff that happened at point 22, but for this `hero_3` object. + + Generates the output: + + ``` + INFO Engine SELECT hero.id AS hero_id, hero.name AS hero_name, hero.secret_name AS hero_secret_name, hero.age AS hero_age + FROM hero + WHERE hero.id = ? + INFO Engine [cached since 0.002215s ago] (3,) + + + Hero 3 ID: 3 + ``` + +25. Print the line `"After committing the session, show names"`. + + Generates the output: + + ``` + After committing the session, show names + ``` + +26. Print the `hero_1.name`. + + Because `hero_1` is still fresh, no additional data is fetched, no additional SQL is executed, and the name is available. + + Generates the output: + + ``` + Hero 1 name: Deadpond + ``` + +27. Print the `hero_2.name`. + + Because `hero_2` is still fresh, no additional data is fetched, no additional SQL is executed, and the name is available. + + Generates the output: + + ``` + Hero 2 name: Spider-Boy + ``` + +28. Print the `hero_3.name`. + + Because `hero_3` is still fresh, no additional data is fetched, no additional SQL is executed, and the name is available. + + Generates the output: + + ``` + Hero 3 name: Rusty-Man + ``` + +29. Explicitly refresh the `hero_1` object. + + The **session** will use the **engine** to execute the SQL necessary to fetch fresh data from the database for the `hero_1` object. + + Generates the output: + + ``` + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.id = ? + INFO Engine [generated in 0.00024s] (1,) + ``` + +30. Explicitly refresh the `hero_2` object. + + The **session** will use the **engine** to execute the SQL necessary to fetch fresh data from the database for the `hero_2` object. + + Generates the output: + + ``` + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.id = ? + INFO Engine [cached since 0.001487s ago] (2,) + ``` + +31. Explicitly refresh the `hero_3` object. + + The **session** will use the **engine** to execute the SQL necessary to fetch fresh data from the database for the `hero_3` object. + + Generates the output: + + ``` + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.id = ? + INFO Engine [cached since 0.002377s ago] (3,) + ``` + +32. Print the line `"After refreshing the heroes"`. + + Generates the output: + + ``` + After refreshing the heroes + ``` + +33. Print the `hero_1`. + + !!! info + Even if the `hero_1` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + + Because the `hero_1` is fresh it has all it's data available. + + Generates the output: + + ``` + Hero 1: age=None id=1 name='Deadpond' secret_name='Dive Wilson' + ``` + +34. Print the `hero_2`. + + !!! info + Even if the `hero_2` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + + Because the `hero_2` is fresh it has all it's data available. + + Generates the output: + + ``` + Hero 2: age=None id=2 name='Spider-Boy' secret_name='Pedro Parqueador' + ``` + +35. Print the `hero_3`. + + !!! info + Even if the `hero_3` wasn't fresh, this would **not** trigger a `refresh` making the **session** use the **engine** to fetch data from the database because it is not accessing an attribute. + + Because the `hero_3` is fresh it has all it's data available. + + Generates the output: + + ``` + Hero 3: age=48 id=3 name='Rusty-Man' secret_name='Tommy Sharp' + ``` + +36. The `with` block ends here (there's no more indented code), so the **session** is closed, running all it's closing code. + + This includes doing a `ROLLBACK` of any possible transaction that could have been started. + + Generates the output: + + ``` + INFO Engine ROLLBACK + ``` + +37. Print the line `"After the session closes"`. + + Generates the output: + + ``` + After the session closes + ``` + +38. Print the `hero_1` after closing the session. + + Generates the output: + + ``` + Hero 1: age=None id=1 name='Deadpond' secret_name='Dive Wilson' + ``` + +39. Print the `hero_2` after closing the session. + + Generates the output: + + ``` + Hero 2: age=None id=2 name='Spider-Boy' secret_name='Pedro Parqueador' + ``` + +40. Print the `hero_3` after closing the session. + + Generates the output: + + ``` + Hero 3: age=48 id=3 name='Rusty-Man' secret_name='Tommy Sharp' + ``` diff --git a/docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md b/docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md new file mode 100644 index 0000000..f20779a --- /dev/null +++ b/docs_src/tutorial/create_db_and_table/annotations/en/tutorial003.md @@ -0,0 +1,75 @@ +1. Import `Optional` from `typing` to declare fields that could be `None`. +2. Import the things we will need from `sqlmodel`: `Field`, `SQLModel`, `create_engine`. +3. Create the `Hero` model class, representing the `hero` table in the database. + + And also mark this class as a **table model** with `table=True`. + +4. Create the `id` field: + + It could be `None` until the database assigns a value to it, so we annotate it with `Optional`. + + It is a **primary key**, so we use `Field()` and the argument `primary_key=True`. + +5. Create the `name` field. + + It is required, so there's no default value, and it's not `Optional`. + +6. Create the `secret_name` field. + + Also required. + +7. Create the `age` field. + + It is not required, the default value is `None`. + + In the database, the default value will be `NULL`, the SQL equivalent of `None`. + + As this field could be `None` (and `NULL` in the database), we annotate it with `Optional`. + +8. Write the name of the database file. +9. Use the name of the database file to create the database URL. +10. Create the engine using the URL. + + This doesn't create the database yet, no file or table is created at this point, only the **engine** object that will handle the connections with this specific database, and with specific support for SQLite (based on the URL). + +11. Put the code that creates side effects in a function. + + In this case, only one line that creates the database file with the table. + +12. Create all the tables that were automatically registered in `SQLModel.metadata`. + +13. Add a main block, or "Top-level script environment". + + And put some logic to be executed when this is called directly with Python, as in: + +
+ + ```console + $ python app.py + + // Execute all the stuff and show the output + ``` + +
+ + ...but that is not executed when importing something from this module, like: + + ```Python + from app import Hero + ``` + +14. In this main block, call the function that creates the database file and the table. + + This way when we call it with: + +
+ + ```console + $ python app.py + + // Doing stuff ✨ + ``` + +
+ + ...it will create the database file and the table. diff --git a/docs_src/tutorial/delete/annotations/en/tutorial002.md b/docs_src/tutorial/delete/annotations/en/tutorial002.md new file mode 100644 index 0000000..130016d --- /dev/null +++ b/docs_src/tutorial/delete/annotations/en/tutorial002.md @@ -0,0 +1,96 @@ +1. Select the hero we will delete. + +2. Execute the query with the select statement object. + + This generates the output: + + ``` + INFO Engine BEGIN (implicit) + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.name = ? + INFO Engine [no key 0.00011s] ('Spider-Youngster',) + ``` + +3. Get one hero object, expecting exactly one. + + !!! tip + This ensures there's no more than one, and that there's exactly one, not `None`. + + This would never return `None`, instead it would raise an exception. + +4. Print the hero object. + + This generates the output: + + ``` + Hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2 + ``` + +5. Delete the hero from the **session**. + + This marks the hero as deleted from the session, but it will not be removed from the database until we **commit** the changes. + +6. Commit the session. + + This saves the changes in the session, including deleting this row. + + It generates the output: + + ``` + INFO Engine DELETE FROM hero WHERE hero.id = ? + INFO Engine [generated in 0.00020s] (2,) + INFO Engine COMMIT + ``` + +7. Print the deleted hero object. + + The hero is deleted in the database. And is marked as deleted in the session. + + But we still have the object in memory with its data, so we can use it to print it. + + This generates the output: + + ``` + Deleted hero: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2 + ``` + +8. Select the same hero again. + + We'll do this to confirm if the hero is really deleted. + +9. Execute the select statement. + + This generates the output: + + ``` + INFO Engine BEGIN (implicit) + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.name = ? + INFO Engine [no key 0.00013s] ('Spider-Youngster',) + ``` + +10. Get the "first" item from the `results`. + + If no items were found, this will return `None`, which is what we expect. + +11. Check if the first item from the results is `None`. + +12. If this first item is indeed `None`, it means that it was correctly deleted from the database. + + Now we can print a message to confirm. + + This generates the output: + + ``` + There's no hero named Spider-Youngster + ``` + +13. This is the end of the `with` block, here the **session** executes its closing code. + + This generates the output: + + ``` + INFO Engine ROLLBACK + ``` diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md new file mode 100644 index 0000000..936b84b --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_001.md @@ -0,0 +1,17 @@ +1. Import the `app` from the the `main` module. + +2. We create a `TestClient` for the FastAPI `app` and put it in the variable `client`. + +3. Then we use use this `client` to **talk to the API** and send a `POST` HTTP operation, creating a new hero. + +4. Then we get the **JSON data** from the response and put it in the variable `data`. + +5. Next we start testing the results with `assert` statements, we check that the status code of the response is `200`. + +6. We check that the `name` of the hero created is `"Deadpond"`. + +7. We check that the `secret_name` of the hero created is `"Dive Wilson"`. + +8. We check that the `age` of the hero created is `None`, because we didn't send an age. + +9. We check that the hero created has an `id` created by the database, so it's not `None`. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md new file mode 100644 index 0000000..0f8555a --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_002.md @@ -0,0 +1,25 @@ +1. Import the `get_session` dependency from the the `main` module. + +2. Define the new function that will be the new **dependency override**. + +3. This function will return a different **session** than the one that would be returned by the original `get_session` function. + + We haven't seen how this new **session** object is created yet, but the point is that this is a different session than the original one from the app. + + This session is attached to a different **engine**, and that different **engine** uses a different URL, for a database just for testing. + + We haven't defined that new **URL** nor the new **engine** yet, but here we already see the that this object `session` will override the one returned by the original dependency `get_session()`. + +4. Then, the FastAPI `app` object has an attribute `app.dependency_overrides`. + + This attribute is a dictionary, and we can put dependency overrides in it by passing, as the **key**, the **original dependency function**, and as the **value**, the **new overriding dependency function**. + + 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) + ``` + +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`. + + This way whenever a *path operation function* needs the dependency FastAPI will use the original one instead of the override. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_003.md b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_003.md new file mode 100644 index 0000000..2b48ebd --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_003.md @@ -0,0 +1,37 @@ +1. Here's a subtle thing to notice. + + Remember that [Order Matters](../create-db-and-table.md#sqlmodel-metadata-order-matters){.internal-link target=_blank} and we need to make sure all the **SQLModel** models are already defined and **imported** before calling `.create_all()`. + + IN this line, by importing something, *anything*, from `.main`, the code in `.main` will be executed, including the definition of the **table models**, and that will automatically register them in `SQLModel.metadata`. + +2. Here we create a new **engine**, completely different from the one in `main.py`. + + This is the engine we will use for the tests. + + We use the new URL of the database for tests: + + ``` + sqlite:///testing.db + ``` + + And again, we use the connection argument `check_same_thread=False`. + +3. Then we call: + + ```Python + SQLModel.metadata.create_all(engine) + ``` + + ...to make sure we create all the tables in the new testing database. + + The **table models** are registered in `SQLModel.metadata` just because we imported *something* from `.main`, and the code in `.main` was executed, creating the classes for the **table models** and automatically registering them in `SQLModel.metadata`. + + So, by the point we call this method, the **table models** are already registered there. 💯 + +4. Here's where we create the custom **session** object for this test in a `with` block. + + It uses the new custom **engine** we created, so anything that uses this session will be using the testing database. + +5. Now, back to the dependency override, it is just returning the same **session** object from outside, that's it, that's the whole trick. + +6. By this point, the testing **session** `with` block finishes, and the session is closed, the file is closed, etc. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md new file mode 100644 index 0000000..92cbe77 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_004.md @@ -0,0 +1,26 @@ +1. Import `StaticPool` from `sqlmodel`, we will use it in a bit. + +2. For the **SQLite URL**, don't write any file name, leave it empty. + + So, instead of: + + ``` + sqlite:///testing.db + ``` + + ...just write: + + ``` + sqlite:// + ``` + + This is enough to tell **SQLModel** (actually SQLAlchemy) that we want to use an **in-memory SQLite database**. + +3. Remember that we told the **low-level** library in charge of communicating with SQLite that we want to be able to **access the database from different threads** with `check_same_thread=False`? + + Now that we use an **in-memory database**, we need to also tell SQLAlchemy that we want to be able to use the **same in-memory database** object from different threads. + + We tell it that with the `poolclass=StaticPool` parameter. + + !!! info + You can read more details in the SQLAlchemy documentation about Using a Memory Database in Multiple Threads diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md new file mode 100644 index 0000000..126e1f1 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_005.md @@ -0,0 +1,41 @@ +1. Import `pytest`. + +2. Use the `@pytest.fixture()` decorator on top of the function to tell pytest that this is a **fixture** function (equivalent to a FastAPI dependency). + + We also give it a name of `"session"`, this will be important in the testing function. + +3. Create the fixture function. This is equivalent to a FastAPI dependency function. + + In this fixture we create the custom **engine**, with the in-memory database, we create the tables, and we create the **session**. + + Then we `yield` the `session` object. + +4. The thing that we `return` or `yield` is what will be available to the test function, in this case, the `session` object. + + Here we use `yield` so that **pytest** comes back to execute "the rest of the code" in this function once the testing function is done. + + We don't have any more visible "rest of the code" after the `yield`, but we have the end of the `with` block that will close the **session**. + + By using `yield`, pytest will: + + * run the first part + * create the **session** object + * give it to the test function + * run the test function + * once the test function is done, it will continue here, right after the `yield`, and will correctly close the **session** object in the end of the `with` block. + +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) + ``` + + ...the way we tell pytest what is the fixture that we want is by using the **exact same name** of the fixture. + + In this case, we named it `session`, so the parameter has to be exactly named `session` for it to work. + + We also add the type annotation `session: Session` so that we can get autocompletion and inline error checks in our editor. + +6. Now in the dependency override function, we just return the same `session` object that came from outside it. + + The `session` object comes from the parameter passed to the test function, and we just re-use it and return it here in the dependency override. diff --git a/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md new file mode 100644 index 0000000..d44a3b6 --- /dev/null +++ b/docs_src/tutorial/fastapi/app_testing/tutorial001/annotations/en/test_main_006.md @@ -0,0 +1,23 @@ +1. Create the new fixture named `"client"`. + +2. This **client fixture**, in turn, also requires the **session fixture**. + +3. Now we create the **dependency override** inside the client fixture. + +4. Set the **dependency override** in the `app.dependency_overrides` dictionary. + +5. Create the `TestClient` with the **FastAPI** `app`. + +6. `yield` the `TestClient` instance. + + By using `yield`, after the test function is done, pytest will come back to execute the rest of the code after `yield`. + +7. This is the cleanup code, after `yield`, and after the test function is done. + + Here we clear the dependency overrides (here it's only one) in the FastAPI `app`. + +8. Now the test function requires the **client fixture**. + + And inside the test function, the code is quite **simple**, we just use the `TestClient` to make requests to the API, check the data, and that's it. + + The fixtures take care of all the **setup** and **cleanup** code. diff --git a/docs_src/tutorial/insert/annotations/en/tutorial003.md b/docs_src/tutorial/insert/annotations/en/tutorial003.md new file mode 100644 index 0000000..8236078 --- /dev/null +++ b/docs_src/tutorial/insert/annotations/en/tutorial003.md @@ -0,0 +1,57 @@ +1. We use a function `create_heroes()` to put this logic together. + +2. Create each of the objects/instances of the `Hero` model. + + Each of them represents the data for one row. + +3. Use a `with` block to create a `Session` using the `engine`. + + The new **sesion** will be assigned to the variable `session`. + + And it will be automatically closed when the `with` block is finished. + +4. Add each of the objects/instances to the **session**. + + Each of these objects represents a row in the database. + + They are all waiting there in the session to be saved. + +5. **Commit** the changes to the database. + + This will actually send the data to the database. + + It will start a transaction automatically and save all the data in a single batch. + +6. By this point, after the `with` block is finished, the **session** is automatically closed. + +7. We have a `main()` function with all the code that should be executed when the program is called as a **script from the console**. + + That way we can add more code later to this function. + + We then put this function `main()` in the main block below. + + And as it is a single function, other Python files could **import it** and call it directly. + +8. In this `main()` function, we are also creating the database and the tables. + + In the previous version, this function was called directly in the main block. + + But now it is just called in the `main()` function. + +9. And now we are also creating the heroes in this `main()` function. + +10. We still have a main block to execute some code when the program is run as a script from the command line, like: + +
+ + ```console + $ python app.py + + // Do whatever is in the main block 🚀 + ``` + +
+ +11. There's a single `main()` function now that contains all the code that should be executed when running the program from the console. + + So this is all we need to have in the main block. Just call the `main()` function. diff --git a/docs_src/tutorial/select/annotations/en/tutorial002.md b/docs_src/tutorial/select/annotations/en/tutorial002.md new file mode 100644 index 0000000..2570b54 --- /dev/null +++ b/docs_src/tutorial/select/annotations/en/tutorial002.md @@ -0,0 +1,63 @@ +1. Import from `sqlmodel` everything we will use, including the new `select()` function. + +2. Create the `Hero` class model, representing the `hero` table. + +3. Create the **engine**, we should use a single one shared by all the application code, and that's what we are doing here. + +4. Create all the tables for the models registered in `SQLModel.metadata`. + + This also creates the database if it doesn't exist already. + +5. Create each one of the `Hero` objects. + + You might not have this in your version if you had already created the data in the database. + +6. Create a new **session** and use it to `add` the heroes to the database, and then `commit` the changes. + +7. Create a new **session** to query data. + + !!! tip + Notice that this is a new **session** independent from the one in the other function above. + + But it still uses the same **engine**. We still have one engine for the whole application. + +8. Use the `select()` function to create a statement selecting all the `Hero` objects. + + This selects all the rows in the `hero` table. + +9. Use `session.exec(statement)` to make the **session** use the **engine** to execute the internal SQL statement. + + This will go to the database, execute that SQL, and get the results back. + + It returns a special iterable object that we put in the variable `results`. + + This generates the output: + + ``` + INFO Engine BEGIN (implicit) + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + INFO Engine [no key 0.00032s] () + ``` + +10. Iterate for each `Hero` object in the `results`. + +11. Print each `hero`. + + The 3 iterations in the `for` loop will generate this output: + + ``` + id=1 name='Deadpond' age=None secret_name='Dive Wilson' + id=2 name='Spider-Boy' age=None secret_name='Pedro Parqueador' + id=3 name='Rusty-Man' age=48 secret_name='Tommy Sharp' + ``` + +12. At this point, after the `with` block, the **session** is closed. + + This generates the output: + + ``` + INFO Engine ROLLBACK + ``` + +13. Add this function `select_heroes()` to the `main()` function so that it is called when we run this program from the command line. diff --git a/docs_src/tutorial/update/annotations/en/tutorial002.md b/docs_src/tutorial/update/annotations/en/tutorial002.md new file mode 100644 index 0000000..3a52bd9 --- /dev/null +++ b/docs_src/tutorial/update/annotations/en/tutorial002.md @@ -0,0 +1,68 @@ +1. Select the hero we will work with. + +2. Execute the query with the select statement object. + + This generates the output: + + ``` + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.name = ? + INFO Engine [no key 0.00017s] ('Spider-Boy',) + ``` + +3. Get one hero object, expecting exactly one. + + !!! tip + This ensures there's no more than one, and that there's exactly one, not `None`. + + This would never return `None`, instead it would raise an exception. + +4. Print the hero object. + + This generates the output: + + ``` + Hero: name='Spider-Boy' secret_name='Pedro Parqueador' age=None id=2 + ``` + +5. Set the hero's age field to the new value `16`. + + Now the `hero` object in memory has a different value for the age, but it is still not saved to the database. + +6. Add the hero to the session. + + This puts it in that temporary place in the session before committing. + + But it's still not saved in the database yet. + +7. Commit the session. + + This saves the updated hero to the database. + + And this generates the output: + + ``` + INFO Engine UPDATE hero SET age=? WHERE hero.id = ? + INFO Engine [generated in 0.00017s] (16, 2) + INFO Engine COMMIT + ``` + +8. Refresh the hero object to have the recent data, including the age we just committed. + + This generates the output: + + ``` + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.id = ? + INFO Engine [generated in 0.00018s] (2,) + ``` + +9. Print the updated hero object. + + This generates the output: + + ``` + Updated hero: name='Spider-Boy' secret_name='Pedro Parqueador' age=16 id=2 + ``` diff --git a/docs_src/tutorial/update/annotations/en/tutorial004.md b/docs_src/tutorial/update/annotations/en/tutorial004.md new file mode 100644 index 0000000..55755cd --- /dev/null +++ b/docs_src/tutorial/update/annotations/en/tutorial004.md @@ -0,0 +1,159 @@ +1. Select the hero `Spider-Boy`. + +2. Execute the select statement. + + This generates the output: + + ``` + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.name = ? + INFO Engine [no key 0.00018s] ('Spider-Boy',) + ``` + +3. Get one hero object, the only one that should be there for **Spider-Boy**. + +4. Print this hero. + + This generates the output: + + ``` + Hero 1: name='Spider-Boy' secret_name='Pedro Parqueador' age=None id=2 + ``` + +5. Select another hero. + +6. Execute the select statement. + + This generates the output: + + ``` + INFO Engine BEGIN (implicit) + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.name = ? + INFO Engine [no key 0.00020s] ('Captain North America',) + ``` + + !!! tip + See the `BEGIN` at the top? + + This is SQLAlchemy automatically starting a transaction for us. + + This way, we could revert the last changes (if there were some) if we wanted to, even if the SQL to create them was already sent to the database. + +7. Get one hero object for this new query. + + The only one that should be there for **Captain North America**. + +8. Print this second hero. + + This generates the output: + + ``` + Hero 2: name='Captain North America' secret_name='Esteban Rogelios' age=93 id=7 + ``` + +9. Update the age for the first hero. + + Set the value of the attribute `age` to `16`. + + This updates the hero object in memory, but not yet in the database. + +10. Update the name of the first hero. + + Now the name of the hero will not be `"Spider-Boy"` but `"Spider-Youngster"`. + + Also, this updates the object in memory, but not yet in the database. + +11. Add this first hero to the session. + + This puts it in the temporary space in the **session** before committing it to the database. + + It is not saved yet. + +12. Update the name of the second hero. + + Now the hero has a bit more precision in the name. 😜 + + This updates the object in memory, but not yet in the database. + +13. Update the age of the second hero. + + This updates the object in memory, but not yet in the database. + +14. Add the second hero to the session. + + This puts it in the temporary space in the **session** before committing it to the database. + +15. Commit all the changes tracked in the session. + + This commits everything in one single batch. + + This generates the output: + + ``` + INFO Engine UPDATE hero SET name=?, age=? WHERE hero.id = ? + INFO Engine [generated in 0.00028s] (('Spider-Youngster', 16, 2), ('Captain North America Except Canada', 110, 7)) + INFO Engine COMMIT + ``` + + !!! tip + See how SQLAlchemy (that powers SQLModel) optimizes the SQL to do as much work as possible in a single batch. + + Here it updates both heroes in a single SQL query. + +16. Refresh the first hero. + + This generates the output: + + ``` + INFO Engine BEGIN (implicit) + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.id = ? + INFO Engine [generated in 0.00023s] (2,) + ``` + + !!! tip + Because we just committed a SQL transaction with `COMMIT`, SQLAlchemy will automatically start a new transaction with `BEGIN`. + +17. Refresh the second hero. + + This generates the output: + + ``` + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + WHERE hero.id = ? + INFO Engine [cached since 0.001709s ago] (7,) + ``` + + !!! tip + SQLAlchemy is still using the previous transaction, so it doesn't have to create a new one. + +18. Print the first hero, now udpated. + + This generates the output: + + ``` + Updated hero 1: name='Spider-Youngster' secret_name='Pedro Parqueador' age=16 id=2 + ``` + +19. Print the second hero, now updated. + + This generates the output: + + ``` + Updated hero 2: name='Captain North America Except Canada' secret_name='Esteban Rogelios' age=110 id=7 + ``` + +20. Here is the end of the `with` block statement, so the session can execute its terminating code. + + The session will `ROLLBACK` (undo) any possible changes in the last transaction that were not committed. + + This generates the output: + + ``` + INFO Engine ROLLBACK + ```