/item")
def get_item_in_store(name):
for store in stores:
if store["name"] == name:
return {"items": store["items"]}
return {"message": "Store not found"}, 404
================================================
FILE: docs/docs/04_docker_intro/05_run_commands_in_docker_containers/README.md
================================================
# How to run commands inside a Docker container
If you run your API using Docker Compose, with the `docker compose up` command, you may also want to be able to execute arbitrary shell commands in the container.
For example, later on in the course we will look at database migrations.
To execute a database migration, we need to run a specific command, `flask db mgirate`.
If we use Docker Compose, we'll need to run the command inside the running container, and not in a local terminal.
You can run any arbitrary command in a running container like so:
```bash
docker compose exec web flask db migrate
```
This command is split into 4 parts:
- `docker compose`: uses the Docker Compose part of the Docker executable
- `exec`: used to run a command in a specific Docker Compose service
- `web`: which Docker Compose service to run the command in
- `flask db migrate`: the command you want to run
That's all! Just remember while following the course, that if I run any commands in my local terminal and you are using Docker Compose, you should precede the commands with `docker compose exec web`.
================================================
FILE: docs/docs/04_docker_intro/README.md
================================================
# An Introduction to Docker
:::caution Not a Docker course
An important foreword: this is not a Docker course, and I'm not a Docker expert!
In this section, and in later sections of this course, I'll teach you what Docker is and how to use it to run and deploy your Flask apps. However, I won't teach you everything there is to know about Docker!
:::
Docker is a software framework for building, running, and managing **images** and **containers**.
In order to understand Docker, you need to clarify two questions:
- What are Docker containers, and how are they different to Virtual Machines?
- What are Docker images?
After this, you'll be ready to create your own Docker images and use those images to create and run containers.
Let's take a look at Docker containers in the next lecture!
================================================
FILE: docs/docs/04_docker_intro/_category_.json
================================================
{
"label": "Introduction to Docker",
"position": 4
}
================================================
FILE: docs/docs/05_flask_smorest/01_why_flask_smorest/README.md
================================================
---
ctslug: why-use-flask-smorest
---
# Why use Flask-Smorest
There are many different REST API libraries for Flask. In a previous version of this course, we used Flask-RESTful. Now, I recommend using [Flask-Smorest](https://github.com/marshmallow-code/flask-smorest).
Over the last few months, I've been trialing the major REST libraries for Flask. I've built REST APIs using Flask-RESTful, Flask-RESTX, and Flask-Smorest.
I was looking to compare the three libraries in a few key areas:
- **Ease of use and getting started**. Many REST APIs are essentially microservices, so being able to whip one up quickly and without having to go through a steep learning curve is definitely interesting.
- **Maintainability and expandability**. Although many start as microservices, sometimes we have to maintain projects for a long time. And sometimes, they grow past what we originally envisioned.
- **Activity in the library itself**. Even if a library is suitable now, if it is not actively maintained and improved, it may not be suitable in the future. We'd like to teach something that you will use for years to come.
- **Documentation and usage of best practice**. The library should help you write better code by having strong documentation and guiding you into following best practice. If possible, it should use existing, actively maintained libraries as dependencies instead of implementing their own versions of them.
- **Developer experience in production projects**. The main point here was: how easy is it to produce API documentation with the library of choice. Hundreds of students have asked me how to integrate Swagger in their APIs, so it would be great if the library we teach gave it to you out of the box.
## Flask-Smorest is the most well-rounded
It ticks all the boxes above:
- If you want, it can be super similar to Flask-RESTful (which is a compliment, really easy to get started!).
- It uses [marshmallow](https://marshmallow.readthedocs.io/en/stable/) for serialization and deserialization, which is a huge plus. Marshmallow is a very actively-maintained library which is very intuitive and unlocks very easy argument validation. Unfortunately Flask-RESTX [doesn't use marshmallow](https://flask-restx.readthedocs.io/en/latest/marshalling.html), though there are [plans to do so](https://github.com/python-restx/flask-restx/issues/59).
- It provides Swagger (with Swagger UI) and other documentations out of the box. It uses the same marshmallow schemas you use for API validation and some simple decorators in your code to generate the documentation.
- The documentation is the weakest point (compared to Flask-RESTX), but with this course we can help you navigate it. The documentation of marshmallow is superb, so that will also help.
## If you took an old version of this course...
Let me tell you about some of the key differences between a project that uses Flask-RESTful and one that uses Flask-Smorest. After reading through these differences, it should be fairly straightforward for you to look at two projects, each using one library, and compare them.
1. Flask-Smorest uses `flask.views.MethodView` classes registered under a `flask_smorest.Blueprint` instead of `flask_restful.Resource` classes.
2. Flask-Smorest uses `flask_smorest.abort` to return error responses instead of manually returning the error JSON and error code.
3. Flask-Smorest projects define marshmallow schemas that represent incoming data (for deserialization and validation) and outgoing data (for serialization). It uses these schemas to automatically validate the data and turn Python objects into JSON.
Throughout this section I'll show you how to implement these 3 points in practice, so if you've already got a REST API that uses Flask-RESTful, you'll find it really easy to migrate.
Of course, you can keep using Flask-RESTful for your existing projects, and only use Flask-Smorest for new projects. That's also an option! Flask-RESTful isn't abandoned or deprecated, so it's still a totally viable option.
================================================
FILE: docs/docs/05_flask_smorest/02_data_model_improvements/README.md
================================================
---
title: "Data model improvements"
description: "Use dictionaries instead of lists for data storage, and store stores and items separately."
ctslug: data-model-improvements
---
# Data model improvements
## Starting code from section 4
This is the "First REST API" project from Section 4:
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
```py title="app.py"
from flask import Flask, request
app = Flask(__name__)
stores = [
{
"name": "My Store",
"items": [
{
"name": "Chair",
"price": 15.99
}
]
}
]
@app.get("/store") # http://127.0.0.1:5000/store
def get_stores():
return {"stores": stores}
@app.post("/store")
def create_store():
request_data = request.get_json()
new_store = {"name": request_data["name"], "items": []}
stores.append(new_store)
return new_store, 201
@app.post("/store//item")
def create_item(name):
request_data = request.get_json()
for store in stores:
if store["name"] == name:
new_item = {"name": request_data["name"], "price": request_data["price"]}
store["items"].append(new_item)
return new_item, 201
return {"message": "Store not found"}, 404
@app.get("/store/")
def get_store(name):
for store in stores:
if store["name"] == name:
return store
return {"message": "Store not found"}, 404
@app.get("/store//item")
def get_item_in_store(name):
for store in stores:
if store["name"] == name:
return {"items": store["items"]}
return {"message": "Store not found"}, 404
```
```docker
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
```
## New files
:::tip Insomnia files
Remember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!
There are two Insomnia files for this section: one for lectures 1-5 (before adding Docker), and one for the other lectures (after adding Docker).
:::
Let's start off by creating a `requirements.txt` file with all our dependencies:
```txt title="requirements.txt"
flask
flask-smorest
python-dotenv
```
We're adding `flask-smorest` to help us write REST APIs more easily, and generate documentation for us.
We're adding `python-dotenv` so it's easier for us to load environment variables and use the `.flaskenv` file.
Next, let's create the `.flaskenv` file:
```txt title=".flaskenv"
FLASK_APP=app
FLASK_DEBUG=True
```
If we have the `python-dotenv` library installed, when we run the `flask run` command, Flask will read the variables inside `.flaskenv` and use them to configure the Flask app.
The configuration that we'll do is to define the Flask app file (here, `app.py`). Then we'll also set the `FLASK_DEBUG` flag to `True`, which does a couple things:
- Makes the app give us better error messages and return a traceback when we make requests if there's an error.
- Sets the app reloading to true, so the app restarts when we make code changes
We don't want debug mode to be enabled in production (when we deploy our app), but while we're doing development it's definitely a time-saving tool!
## Code improvements
### Creating a database file
First of all, let's move our "database" to another file.
Create a `db.py` file with the following content:
```py title="db.py"
stores = {}
items = {}
```
In the existing code we only have a `stores` list, so delete that from `app.py`. From now on we will be storing information about items and stores separately.
:::tip What is in each dictionary?
Each dictionary will closely mimic how a database works: a mapping of ID to data. So each dictionary will be something like this:
```py
{
1: {
"name": "Chair",
"price": 17.99
},
2: {
"name": "Table",
"price": 180.50
}
}
```
This will make it much easier to retrieve a specific store or item, just by knowing its ID.
:::
Then, import the `stores` and `items` variables from `db.py` in `app.py`:
```py title="app.py"
from db import stores, items
```
## Using stores and items in our API
Now let's make use of stores and items separately in our API.
### `get_store`
Here are the changes we'll need to make:
```py title="app.py"
@app.get("/store/")
def get_store(name):
for store in stores:
if store["name"] == name:
return store
return {"message": "Store not found"}, 404
```
```py title="app.py"
@app.get("/store/")
def get_store(store_id):
try:
# Here you might also want to add the items in this store
# We'll do that later on in the course
return stores[store_id]
except KeyError:
return {"message": "Store not found"}, 404
```
Important to note that in this version, we won't return the items in the store. That's a limitation of our dictionaries-for-database setup that we will solve when we introduce databases!
### `get_stores`
```py title="app.py"
@app.get("/store")
def get_stores():
return {"stores": stores}
```
```py title="app.py"
@app.get("/store")
def get_stores():
return {"stores": list(stores.values())}
```
### `create_store`
```py title="app.py"
@app.post("/store")
def create_store():
request_data = request.get_json()
new_store = {"name": request_data["name"], "items": []}
stores.append(new_store)
return new_store, 201
```
```py title="app.py"
import uuid
@app.post("/store")
def create_store():
store_data = request.get_json()
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
```
Here we add a new import, [the `uuid` module](https://docs.python.org/3/library/uuid.html). We will be using it to create unique IDs for our stores and items instead of relying on the uniqueness of their names.
### `create_item`
```py title="app.py"
@app.post("/store//item")
def create_item(name):
request_data = request.get_json()
for store in stores:
if store["name"] == name:
new_item = {"name": request_data["name"], "price": request_data["price"]}
store["items"].append(new_item)
return new_item, 201
return {"message": "Store not found"}, 404
```
```py title="app.py"
@app.post("/item")
def create_item():
item_data = request.get_json()
if item_data["store_id"] not in stores:
return {"message": "Store not found"}, 404
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
```
Now we are POSTing to `/item` instead of `/store//item`. The endpoint will expect to receive JSON with `price`, `name`, and `store_id`.
### `get_items` (new)
This is not an endpoint we could easily make when we were working with a single `stores` list!
```py
@app.get("/item")
def get_all_items():
return {"items": list(items.values())}
```
### `get_item_in_store`
```py title="app.py"
@app.get("/store//item")
def get_item_in_store(name):
for store in stores:
if store["name"] == name:
return {"items": store["items"]}
return {"message": "Store not found"}, 404
```
```py title="app.py"
@app.get("/item/")
def get_item(item_id):
try:
return items[item_id]
except KeyError:
return {"message": "Item not found"}, 404
```
Now we are GETting from `/item` instead of `/store//item`. This is because while items are related to stores, they aren't inside a store anymore!
================================================
FILE: docs/docs/05_flask_smorest/02_data_model_improvements/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/02_data_model_improvements/end/Dockerfile
================================================
# In the course we run the app outside Docker
# until lecture 5.
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/02_data_model_improvements/end/app.py
================================================
import uuid
from flask import Flask, request
from db import stores, items
app = Flask(__name__)
@app.get("/item/")
def get_item(item_id):
try:
return items[item_id]
except KeyError:
return {"message": "Item not found"}, 404
@app.post("/item")
def create_item():
item_data = request.get_json()
if item_data["store_id"] not in stores:
return {"message": "Store not found"}, 404
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
@app.get("/item")
def get_all_items():
return {"items": list(items.values())}
@app.get("/store/")
def get_store(store_id):
try:
# Here you might also want to add the items in this store
# We'll do that later on in the course
return stores[store_id]
except KeyError:
return {"message": "Store not found"}, 404
@app.post("/store")
def create_store():
store_data = request.get_json()
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
@app.get("/store")
def get_stores():
return {"stores": list(stores.values())}
================================================
FILE: docs/docs/05_flask_smorest/02_data_model_improvements/end/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/02_data_model_improvements/end/requirements.txt
================================================
flask
flask-smorest
python-dotenv
================================================
FILE: docs/docs/05_flask_smorest/02_data_model_improvements/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/02_data_model_improvements/start/app.py
================================================
from flask import Flask, request
app = Flask(__name__)
stores = [{"name": "My Store", "items": [{"name": "Chair", "price": 15.99}]}]
@app.get("/store") # http://127.0.0.1:5000/store
def get_stores():
return {"stores": stores}
@app.post("/store")
def create_store():
request_data = request.get_json()
new_store = {"name": request_data["name"], "items": []}
stores.append(new_store)
return new_store, 201
@app.post("/store//item")
def create_item(name):
request_data = request.get_json()
for store in stores:
if store["name"] == name:
new_item = {"name": request_data["name"], "price": request_data["price"]}
store["items"].append(new_item)
return new_item, 201
return {"message": "Store not found"}, 404
@app.get("/store/")
def get_store(name):
for store in stores:
if store["name"] == name:
return store
return {"message": "Store not found"}, 404
@app.get("/store//item")
def get_item_in_store(name):
for store in stores:
if store["name"] == name:
return {"items": store["items"]}
return {"message": "Store not found"}, 404
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/README.md
================================================
---
title: "Improvements to our first REST API"
description: "Add new error handling and code improvements to the REST API before adding any new endpoints."
ctslug: improvements-to-our-first-rest-api
---
# Improvements to our first REST API
## Using `flask_smorest.abort` instead of returning errors manually
At the moment in our API we're doing things like these in case of an error:
```py title="app.py"
@app.get("/store/")
def get_store(name):
try:
# Here you might also want to add the items in this store
# We'll do that later on in the course
return stores[store_id]
except KeyError:
return {"message": "Store not found"}, 404
```
A small improvement we can do on this is use the `abort` function from Flask-Smorest, which helps us write these messages and include a bit of extra information too.
Add this import at the top of `app.py`:
```py title="app.py"
from flask_smorest import abort
```
And then let's change our error returns to use `abort`.
```py title="app.py"
@app.get("/store/")
def get_store(store_id):
try:
# Here you might also want to add the items in this store
# We'll do that later on in the course
return stores[store_id]
except KeyError:
# highlight-start
abort(404, message="Store not found.")
# highlight-end
```
And here:
```py title="app.py"
@app.get("/item/")
def get_item(item_id):
try:
return items[item_id]
except KeyError:
# highlight-start
abort(404, message="Item not found.")
# highlight-end
```
## Adding error handling on creating items and stores
At the moment when we create items and stores, we _expect_ there to be certain items in the JSON body of the request.
If those items are missing, the app will return an error 500, which means "Internal Server Error".
Instead of that, it's good practice to return an error 400 and a message telling the client what went wrong.
To do so, let's inspect the body of the request and see if it contains the data we need.
Let's change our `create_item()` function to this:
```py title="app.py"
@app.post("/item")
def create_item():
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
```
And our `create_store()` function to this:
```py title="app.py"
@app.post("/store")
def create_store():
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
```
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/end/app.py
================================================
import uuid
from flask import Flask, request
from flask_smorest import abort
from db import stores, items
app = Flask(__name__)
@app.get("/item/")
def get_item(item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
@app.post("/item")
def create_item():
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
@app.get("/item")
def get_all_items():
return {"items": list(items.values())}
@app.get("/store/")
def get_store(store_id):
try:
# Here you might also want to add the items in this store
# We'll do that later on in the course
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
@app.post("/store")
def create_store():
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
@app.get("/store")
def get_stores():
return {"stores": list(stores.values())}
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/end/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/end/requirements.txt
================================================
flask
flask-smorest
python-dotenv
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/start/app.py
================================================
import uuid
from flask import Flask, request
from db import stores, items
app = Flask(__name__)
@app.get("/item/")
def get_item(item_id):
try:
return items[item_id]
except KeyError:
return {"message": "Item not found"}, 404
@app.post("/item")
def create_item():
item_data = request.get_json()
if item_data["store_id"] not in stores:
return {"message": "Store not found"}, 404
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
@app.get("/item")
def get_all_items():
return {"items": list(items.values())}
@app.get("/store/")
def get_store(store_id):
try:
# Here you might also want to add the items in this store
# We'll do that later on in the course
return stores[store_id]
except KeyError:
return {"message": "Store not found"}, 404
@app.post("/store")
def create_store():
store_data = request.get_json()
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
@app.get("/store")
def get_stores():
return {"stores": list(stores.values())}
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/start/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/03_improvements_on_first_rest_api/start/requirements.txt
================================================
flask
flask-smorest
python-dotenv
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/README.md
================================================
---
title: "New endpoints for our REST API"
description: "Let's add a few routes to our first REST API, so it better matches what a production REST API would look like."
ctslug: new-endpoints-for-our-rest-api
---
# New endpoints for our REST API
## New endpoints
We want to add some endpoints for added functionality:
- `DELETE /item/` so we can delete items from the database.
- `PUT /item/` so we can update items.
- `DELETE /store/` so we can delete stores.
### Deleting items
This is almost identical to getting items, but we use the `del` keyword to remove the entry from the dictionary.
```py title="app.py"
@app.delete("/item/")
def delete_item(item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
```
### Updating items
This is almost identical to creating items, but in this API we've decided to not let item updates change the `store_id` of the item. So clients can change item name and price, but not the store that the item belongs to.
This is an API design decision, and you could very well allow clients to update the `store_id` if you want!
```py title="app.py"
@app.put("/item/")
def update_item(item_id):
item_data = request.get_json()
# There's more validation to do here!
# Like making sure price is a number, and also both items are optional
# You should also prevent keys that aren't 'price' or 'name' to be passed
# Difficult to do with an if statement...
if "price" not in item_data or "name" not in item_data:
abort(
400,
message="Bad request. Ensure 'price', and 'name' are included in the JSON payload.",
)
try:
item = items[item_id]
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
```
:::tip Dictionary update operators
The `|=` syntax is a new dictionary operator. You can read more about it [here](https://blog.teclado.com/python-dictionary-merge-update-operators/).
:::
### Deleting stores
This is very similar to deleting items!
```py title="app.py"
@app.delete("/store/")
def delete_store(store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
```
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/end/app.py
================================================
import uuid
from flask import Flask, request
from flask_smorest import abort
from db import stores, items
app = Flask(__name__)
@app.get("/item/")
def get_item(item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
@app.post("/item")
def create_item():
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
@app.delete("/item/")
def delete_item(item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
@app.put("/item/")
def update_item(item_id):
item_data = request.get_json()
# There's more validation to do here!
# Like making sure price is a number, and also both items are optional
# Difficult to do with an if statement...
if "price" not in item_data or "name" not in item_data:
abort(
400,
message="Bad request. Ensure 'price', and 'name' are included in the JSON payload.",
)
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@app.get("/item")
def get_all_items():
return {"items": list(items.values())}
@app.get("/store/")
def get_store(store_id):
try:
# Here you might also want to add the items in this store
# We'll do that later on in the course
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
@app.post("/store")
def create_store():
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
@app.delete("/store/")
def delete_store(store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@app.get("/store")
def get_stores():
return {"stores": list(stores.values())}
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/end/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/end/requirements.txt
================================================
flask
flask-smorest
python-dotenv
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/start/app.py
================================================
import uuid
from flask import Flask, request
from flask_smorest import abort
from db import stores, items
app = Flask(__name__)
@app.get("/item/")
def get_item(item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
@app.post("/item")
def create_item():
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
@app.get("/item")
def get_all_items():
return {"items": list(items.values())}
@app.get("/store/")
def get_store(store_id):
try:
# Here you might also want to add the items in this store
# We'll do that later on in the course
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
@app.post("/store")
def create_store():
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
@app.get("/store")
def get_stores():
return {"stores": list(stores.values())}
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/start/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/04_new_endpoints_for_api/start/requirements.txt
================================================
flask
flask-smorest
python-dotenv
================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/README.md
================================================
---
title: "Reloading API code in Docker container"
description: "Learn how to get your code instantly synced up to the Docker container, so that every time you make a code change it restarts the app in the container and uses the latest code."
ctslug: reloading-api-code-in-docker-container
---
# Reloading API code in Docker container
## Updating Dockerfile to use `requirements.txt`
This is the Dockerfile as we've got it:
```dockerfile
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
```
But there is a problem! It doesn't use the `requirements.txt`, so it only installs Flask as a dependency.
We want to add `requirements.txt` and install the dependencies from it. You might be tempted to move the `COPY` line above the `RUN` line, and then install it with `pip install -r requirements.txt`.
But there's a better way!
```dockerfile
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
```
Here we:
- Add a new `COPY` line that copies the `requirements.txt` file into the image. This creates a new cached layer, so that if the `requirements.txt` file doesn't change, this line and the following `RUN` line don't run again.
- Change the `pip install` code to use `--no-cache-dir --upgrade`. This makes sure that we don't use any pre-existing pip caches when installing, and also upgrades libraries to the latest version if necessary.
## Running the container with volumes for hot reloading
Up to now, we've been re-building the Docker image and re-running the container each time we make a code change.
This is a bit of a time sink, and a bit annoying to do! Let's do it so that the Docker container runs the code that we're editing. That way, when we make a change to the code, the Flask app should restart and use the new code.
All we have to do is:
1. Build the Docker image
2. Run the image, but replace the contents of the image's `/app` directory (where the code is) by the contents of our source code folder in the host machine.
So, first build the Docker image:
```
docker build -t flask-smorest-api .
```
Once that's done, the image has an `/app` directory which contains the source code as it was copied from the host machine during the build stage.
So at this point, we _can_ run a container from this image, and it will run the app _as it was when it was built_:
```
docker run -dp 5000:5000 flask-smorest-api
```
This should just work, and you can try it out in the Insomnia REST Client to make sure the endpoints all work.
But like we said earlier, when we make changes to the code we'll have to rebuild and rerun.
So instead, what we can do is run the image, but replace the image's `/app` directory with the host's source code folder.
That will cause the source code to change in the Docker container while it's running. And, since we've ran Flask with debug mode on, the Flask app will automatically restart when the code changes.
To do so, stop the running container (if you have one running), and use this command instead:
```
docker run -dp 5000:5000 -w /app -v "$(pwd):/app" flask-smorest-api
```
:::info Windows command
The command on Windows varies depending on what terminal application you use. Here are some of the most popular ones!
**PowerShell**
```
docker run -dp 5000:5000 -w //app -v "$(Get-Location)://app" flask-smorest-api
```
**Git Bash**
```
docker run -dp 5000:5000 -w //app -v "//$(pwd)://app" flask-smorest-api
```
**Command Prompt (CMD)**
```
docker run -dp 5000:5000 -w //app -v "%cd%://app" flask-smorest-api
```
:::
- `-dp 5000:5000` - same as before. Run in detached (background) mode and create a port mapping.
- `-w /app` - sets the container's present working directory where the command will run from.
- `-v "$(pwd):/app"` - bind mount (link) the host's present directory to the container's `/app` directory. Note: Docker requires absolute paths for binding mounts, so in this example we use `pwd` for printing the absolute path of the working directory instead of typing it manually.
- `flask-smorest-api` - the image to use.
And with this, your Docker container now is running the code as shown in your IDE. Plus, since Flask is running with debug mode on, the Flask app will restart when you make code changes!
:::info
Using this kind of volume mapping only makes sense _during development_. When you share your Docker image or deploy it, you won't be sharing anything from the host to the container. That's why it's still important to include the original source code in the image when you build it.
:::
Just to recap, here are the two ways we've seen to run your Docker container:

================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/end/app.py
================================================
import uuid
from flask import Flask, request
from flask_smorest import abort
from db import stores, items
app = Flask(__name__)
@app.get("/item/")
def get_item(item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
@app.post("/item")
def create_item():
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
@app.delete("/item/")
def delete_item(item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
@app.put("/item/")
def update_item(item_id):
item_data = request.get_json()
# There's more validation to do here!
# Like making sure price is a number, and also both items are optional
# Difficult to do with an if statement...
if "price" not in item_data or "name" not in item_data:
abort(
400,
message="Bad request. Ensure 'price', and 'name' are included in the JSON payload.",
)
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@app.get("/item")
def get_all_items():
return {"items": list(items.values())}
@app.get("/store/")
def get_store(store_id):
try:
# Here you might also want to add the items in this store
# We'll do that later on in the course
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
@app.post("/store")
def create_store():
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
@app.delete("/store/")
def delete_store(store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@app.get("/store")
def get_stores():
return {"stores": list(stores.values())}
================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/end/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/end/requirements.txt
================================================
flask
flask-smorest
python-dotenv
================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
RUN pip install flask
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/start/app.py
================================================
import uuid
from flask import Flask, request
from flask_smorest import abort
from db import stores, items
app = Flask(__name__)
@app.get("/item/")
def get_item(item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
@app.post("/item")
def create_item():
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
@app.delete("/item/")
def delete_item(item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
@app.put("/item/")
def update_item(item_id):
item_data = request.get_json()
# There's more validation to do here!
# Like making sure price is a number, and also both items are optional
# Difficult to do with an if statement...
if "price" not in item_data or "name" not in item_data:
abort(
400,
message="Bad request. Ensure 'price', and 'name' are included in the JSON payload.",
)
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@app.get("/item")
def get_all_items():
return {"items": list(items.values())}
@app.get("/store/")
def get_store(store_id):
try:
# Here you might also want to add the items in this store
# We'll do that later on in the course
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
@app.post("/store")
def create_store():
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
@app.delete("/store/")
def delete_store(store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@app.get("/store")
def get_stores():
return {"stores": list(stores.values())}
================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/start/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/05_reload_api_docker_container/start/requirements.txt
================================================
flask
flask-smorest
python-dotenv
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/README.md
================================================
---
title: How to use Blueprints and MethodViews
description: Flask-Smorest MethodViews allow us to simplify API Resources by defining all methods that interact with the resource in one Python class.
ctslug: how-to-use-flask-smorest-methodviews-blueprints
---
# How to use Flask-Smorest MethodViews and Blueprints
Let's improve the structure of our code by splitting items and stores endpoints into their own files.
Let's create a `resources` folder, and inside it create `item.py` and `store.py`.
## Creating a blueprint for each related group of resources
### `resources/store.py`
Let's start in `store.py`, and create a `Blueprint`:
```py title="resources/store.py"
import uuid
from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import stores
blp = Blueprint("stores", __name__, description="Operations on stores")
```
The `Blueprint` arguments are the same as the Flask `Blueprint`[^1], with an added optional `description` keyword argument:
1. `"stores"` is the name of the blueprint. This will be shown in the documentation and is prepended to the endpoint names when you use `url_for` (we won't use it).
2. `__name__` is the "import name".
3. The `description` will be shown in the documentation UI.
Now that we've got this, let's add our `MethodView`s. These are classes where each method maps to one endpoint. The interesting thing is that method names are important:
```py title="resources/store.py"
@blp.route("/store/")
class Store(MethodView):
def get(self, store_id):
pass
def delete(self, store_id):
pass
```
Two things are going on here:
1. The endpoint is associated to the `MethodView` class. Here, the class is called `Store` and the endpoint is `/store/`.
2. There are two methods inside the `Store` class: `get` and `delete`. These are going to map directly to `GET /store/` and `DELETE /store/`.
Now we can copy the code from earlier into each of the methods:
```py title="resources/store.py"
@blp.route("/store/")
class Store(MethodView):
def get(self, store_id):
try:
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
def delete(self, store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
```
Now, still inside the same file, we can add another `MethodView` with a different endpoint, for the `/store` route:
```py title="resources/store.py"
@blp.route("/store")
class StoreList(MethodView):
def get(self):
return {"stores": list(stores.values())}
def post(self):
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
```
### `resources/item.py`
Let's do the same thing with the `resources/item.py` file:
```py title="resources/item.py"
import uuid
from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import items
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
def get(self, item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
def delete(self, item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
def put(self, item_id):
item_data = request.get_json()
# There's more validation to do here!
# Like making sure price is a number, and also both items are optional
# Difficult to do with an if statement...
if "price" not in item_data or "name" not in item_data:
abort(
400,
message="Bad request. Ensure 'price', and 'name' are included in the JSON payload.",
)
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@blp.route("/item")
class ItemList(MethodView):
def get(self):
return {"items": list(items.values())}
def post(self):
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
```
## Import blueprints and Flask-Smorest configuration
Finally, we have to import the `Blueprints` inside `app.py`, and register them with Flask-Smorest:
```py title="app.py"
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
```
I've also added a few config variables to the `app.config`. The `PROPAGATE_EXCEPTIONS` value is used so that when an exception is raised in an extension, it is bubbled up to the main Flask app so you'd see it more easily.
The other config values are there for the documentation of our API, and they define things such as the API name and version, as well as information for the Swagger UI.
Now you should be able to go to `http://127.0.0.1:5000/swagger-ui` and see your Swagger documentation rendered out!
[^1]: [Flask Blueprint (Flask Official Documentation)](https://flask.palletsprojects.com/en/2.1.x/api/#flask.Blueprint)
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/end/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/end/requirements.txt
================================================
flask
flask-smorest
python-dotenv
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/end/resources/item.py
================================================
import uuid
from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import items
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
def get(self, item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
def delete(self, item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
def put(self, item_id):
item_data = request.get_json()
# There's more validation to do here!
# Like making sure price is a number, and also both items are optional
# Difficult to do with an if statement...
if "price" not in item_data or "name" not in item_data:
abort(
400,
message="Bad request. Ensure 'price', and 'name' are included in the JSON payload.",
)
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@blp.route("/item")
class ItemList(MethodView):
def get(self):
return {"items": list(items.values())}
def post(self):
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/end/resources/store.py
================================================
import uuid
from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import stores
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
def get(self, store_id):
try:
# You presumably would want to include the store's items here too
# More on that when we look at databases
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
def delete(self, store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@blp.route("/store")
class StoreList(MethodView):
def get(self):
return {"stores": list(stores.values())}
def post(self):
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/start/app.py
================================================
import uuid
from flask import Flask, request
from flask_smorest import abort
from db import stores, items
app = Flask(__name__)
@app.get("/item/")
def get_item(item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
@app.delete("/item/")
def delete_item(item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
@app.put("/item/")
def update_item(item_id):
item_data = request.get_json()
# There's more validation to do here!
# Like making sure price is a number, and also both items are optional
# Difficult to do with an if statement...
if "price" not in item_data or "name" not in item_data:
abort(
400,
message="Bad request. Ensure 'price', and 'name' are included in the JSON payload.",
)
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@app.get("/item")
def get_all_items():
return {"items": list(items.values())}
@app.post("/item")
def create_item():
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
@app.get("/store/")
def get_store(store_id):
try:
# You presumably would want to include the store's items here too
# More on that when we look at databases
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
@app.delete("/store/")
def delete_store(store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@app.get("/store")
def get_stores():
return {"stores": list(stores.values())}
@app.post("/store")
def create_store():
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/start/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/06_api_with_method_views/start/requirements.txt
================================================
flask
python-dotenv
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/README.md
================================================
---
title: Adding marshmallow schemas
description: A marshmallow schema is useful for validation and serialization. Learn how to write them in this lecture.
ctslug: adding-marshmallow-schemas
---
# Adding marshmallow schemas
Something that we're lacking in our API at the moment is validation. We've done a _tiny_ bit of it with this kind of code:
```py
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
```
But there's so much more we can do. For starters, some data points may be optional in some endpoints. We also want to check the data type is correct (i.e. `price` shouldn't be a string, for example).
To do this kind of checking we can construct a massive `if` statement, or we can use a library that is made specifically for it.
The `marshmallow`[^1] library is used to define _what_ data fields we want, and then we can pass incoming data through the validator. We can also go the other way round, and give it a Python object which `marshmallow` then turns into a dictionary.
## Writing the `ItemSchema`
Here's the definition of an `Item` using `marshmallow` (this is called a **schema**):
```py title="schemas.py"
from marshmallow import Schema, fields
class ItemSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
store_id = fields.Str(required=True)
```
A couple of weird things maybe!
The `id` field is a string, but it has the `dump_only=True` argument. This means that when we use marshmallow to _validate incoming data_, the `id` field won't be used or expected. However, when we use marshmallow to _serialize_ data to be returned to a client, the `id` field will be included in the output.
The other fields will be used for both validation and serialization, and since they have the `required=True` argument, that means that when we do validation if the fields are not present, an error will be raised.
`marshmallow` will also check the data type with `fields.Float` and `fields.Int`.
## Writing the `ItemUpdateSchema`
Something that even to do this day sits a bit weird with me is having multiple different schemas for different applications.
When we want to update an Item, we have different requirements than when we want to create an item.
The main difference is that the incoming data to our API when we update an item is different than when we create one. Fields are optional, such that not all item fields should be required. Also, you may not want to allow certain fields _at all_.
This is the `ItemUpdateSchema`:
```py title="schemas.py"
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
```
As you can see, these are not `required=True`. I've also taken off the `id` and `store_id` fields, because:
- This schema will only be used for incoming data, and we will never receive an `id`.
- We don't want clients to be able to change the `store_id` of an item. If you wanted to allow this, you can add the `store_id` field here as well.
## Writing the `StoreSchema`
```py title="schemas.py"
class StoreSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
```
There's not much to explain here! Similar to the `ItemSchema`, we have `id` and `name` since those are the only fields we need for a store.
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/end/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/end/requirements.txt
================================================
flask
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/end/resources/item.py
================================================
import uuid
from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import items
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
def get(self, item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
def delete(self, item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
def put(self, item_id):
item_data = request.get_json()
# There's more validation to do here!
# Like making sure price is a number, and also both items are optional
# Difficult to do with an if statement...
if "price" not in item_data or "name" not in item_data:
abort(
400,
message="Bad request. Ensure 'price', and 'name' are included in the JSON payload.",
)
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@blp.route("/item")
class ItemList(MethodView):
def get(self):
return {"items": list(items.values())}
def post(self):
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/end/resources/store.py
================================================
import uuid
from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import stores
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
def get(cls, store_id):
try:
# You presumably would want to include the store's items here too
# More on that when we look at databases
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
def delete(cls, store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@blp.route("/store")
class StoreList(MethodView):
def get(cls):
return {"stores": list(stores.values())}
def post(cls):
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/end/schemas.py
================================================
from marshmallow import Schema, fields
class ItemSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
store_id = fields.Str(required=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/start/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/start/requirements.txt
================================================
flask
flask-smorest
python-dotenv
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/start/resources/item.py
================================================
import uuid
from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import items
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
def get(self, item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
def delete(self, item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
def put(self, item_id):
item_data = request.get_json()
# There's more validation to do here!
# Like making sure price is a number, and also both items are optional
# Difficult to do with an if statement...
if "price" not in item_data or "name" not in item_data:
abort(
400,
message="Bad request. Ensure 'price', and 'name' are included in the JSON payload.",
)
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@blp.route("/item")
class ItemList(MethodView):
def get(self):
return {"items": list(items.values())}
def post(self):
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
================================================
FILE: docs/docs/05_flask_smorest/07_marshmallow_schemas/start/resources/store.py
================================================
import uuid
from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import stores
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
def get(cls, store_id):
try:
# You presumably would want to include the store's items here too
# More on that when we look at databases
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
def delete(cls, store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@blp.route("/store")
class StoreList(MethodView):
def get(cls):
return {"stores": list(stores.values())}
def post(cls):
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/README.md
================================================
---
title: Validation with marshmallow
description: We can use the marshmallow library to validate request data from our API clients.
ctslug: validation-with-marshmallow
---
# Validation with marshmallow
Now that we've got our schemas written, let's use them to validate incoming data to our API.
With Flask-Smorest, this couldn't be easier!
Let's start with `resources/item.py`
## Validation in `resources/item.py`
At the top of the file, import the schemas:
```py
from schemas import ItemSchema, ItemUpdateSchema
```
We have two sets of data that may be incoming (in the JSON body of a request): new items and updating items.
So let's go to the `ItemList#post` method and make a couple changes!
First, let's get rid of the existing data validation. Delete the highlighted lines below:
```py
def post(self):
# highlight-start
item_data = request.get_json()
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
# highlight-end
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
```
Now, I know what you're thinking! What about `item_data`? Do we not need to keep that?
When we use `marshmallow` for validation with Flask-Smorest, it will inject the validated data into our method for us.
Look at these two highlighted lines:
```py
# highlight-start
@blp.arguments(ItemSchema)
def post(self, item_data):
# highlight-end
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
```
Nice!
Plus, doing this also adds to your Swagger UI documentation.
Let's do the same when updating items:
```py
# highlight-start
@blp.arguments(ItemUpdateSchema)
def put(self, item_data, item_id):
# highlight-end
try:
item = items[item_id]
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
```
:::caution Order of parameters
Be careful here since we've now got `item_data` and `item_id`. The URL arguments come in at the end. The injected arguments are passed first, so `item_data` goes before `item_id` in our function signature.
:::
## Validation in `resources/store.py`
Now let's do the same in `store.py`!
At the top of the file, import the schema:
```py
from schemas import StoreSchema
```
When creating a store, we'll have this:
```py
@blp.arguments(StoreSchema)
def post(cls, store_data):
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
```
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/requirements.txt
================================================
flask
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/resources/item.py
================================================
import uuid
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from schemas import ItemSchema, ItemUpdateSchema
from db import items
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
def get(self, item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
def delete(self, item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
@blp.arguments(ItemUpdateSchema)
def put(self, item_data, item_id):
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@blp.route("/item")
class ItemList(MethodView):
def get(self):
return {"items": list(items.values())}
@blp.arguments(ItemSchema)
def post(self, item_data):
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/resources/store.py
================================================
import uuid
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import stores
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
def get(cls, store_id):
try:
# You presumably would want to include the store's items here too
# More on that when we look at databases
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
def delete(cls, store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@blp.route("/store")
class StoreList(MethodView):
def get(cls):
return {"stores": list(stores.values())}
@blp.arguments(StoreSchema)
def post(cls, store_data):
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/end/schemas.py
================================================
from marshmallow import Schema, fields
class ItemSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
store_id = fields.Str(required=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/requirements.txt
================================================
flask
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/resources/item.py
================================================
import uuid
from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import items
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
def get(self, item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
def delete(self, item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
def put(self, item_id):
item_data = request.get_json()
# There's more validation to do here!
# Like making sure price is a number, and also both items are optional
# Difficult to do with an if statement...
if "price" not in item_data or "name" not in item_data:
abort(
400,
message="Bad request. Ensure 'price', and 'name' are included in the JSON payload.",
)
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@blp.route("/item")
class ItemList(MethodView):
def get(self):
return {"items": list(items.values())}
def post(self):
item_data = request.get_json()
# Here not only we need to validate data exists,
# But also what type of data. Price should be a float,
# for example.
if (
"price" not in item_data
or "store_id" not in item_data
or "name" not in item_data
):
abort(
400,
message="Bad request. Ensure 'price', 'store_id', and 'name' are included in the JSON payload.",
)
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/resources/store.py
================================================
import uuid
from flask import request
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import stores
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
def get(cls, store_id):
try:
# You presumably would want to include the store's items here too
# More on that when we look at databases
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
def delete(cls, store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@blp.route("/store")
class StoreList(MethodView):
def get(cls):
return {"stores": list(stores.values())}
def post(cls):
store_data = request.get_json()
if "name" not in store_data:
abort(
400,
message="Bad request. Ensure 'name' is included in the JSON payload.",
)
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
================================================
FILE: docs/docs/05_flask_smorest/08_validation_with_marshmallow/start/schemas.py
================================================
from marshmallow import Schema, fields
class ItemSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
store_id = fields.Str(required=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/README.md
================================================
---
title: Decorating responses with Flask-Smorest
description: Add response serialization and status code to API endpoints, and add to your documentation in the process.
ctslug: decorating-responses-with-flask-smorest
---
# Decorating responses with Flask-Smorest
We can use marshmallow schemas for serialization when we respond to a client. To do so, we need to tell Flask-Smorest what Schema to use when responding.
This will do a few things:
1. Update your documentation to show what data and status code will be returned by the endpoint.
2. Pass any data your endpoint returns through the marshmallow schema, casting data types and removing data that isn't in the schema.
## Decorating responses in `resources/item.py`
Let's start with retrieving a specific item.
Up until now, we've been doing this:
```py
def get(self, item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
```
But now we can run the `items[item_id]` dictionary through the marshmallow schema and tell Flask-Smorest about it so the documentation will be updated:
```py
@blp.response(200, ItemSchema)
def get(self, item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
```
:::info
The number, `200`, is the status code. It means "OK" (all good).
:::
Our endpoint for updating items looks like this:
```py
@blp.arguments(ItemUpdateSchema)
def put(self, item_data, item_id):
try:
item = items[item_id]
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
```
Let's pass this through the schema as well:
```py
@blp.arguments(ItemUpdateSchema)
# highlight-start
@blp.response(200, ItemSchema)
# highlight-end
def put(self, item_data, item_id):
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
```
:::caution
Careful with the order of decorators in these functions!
:::
When we get to returning a list of items, it looks like this:
```py
# highlight-start
@blp.response(200, ItemSchema(many=True))
# highlight-end
def get(self):
return items.values()
```
And finally, don't forget to decorate the new item endpoint too:
```py
@blp.arguments(ItemSchema)
# highlight-start
@blp.response(201, ItemSchema)
# highlight-end
def post(self, item_data):
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
```
## Decorating responses in `resources/store.py`
Going a bit more quickly here since you already know what's going on with this decorator. The highlighted lines are new:
```py title="resources/store.py"
import uuid
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import stores
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
# highlight-start
@blp.response(200, StoreSchema)
# highlight-end
def get(cls, store_id):
try:
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
def delete(cls, store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@blp.route("/store")
class StoreList(MethodView):
# highlight-start
@blp.response(200, StoreSchema(many=True))
# highlight-end
def get(cls):
return stores.values()
@blp.arguments(StoreSchema)
# highlight-start
@blp.response(201, StoreSchema)
# highlight-end
def post(cls, store_data):
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
```
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/end/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/end/requirements.txt
================================================
flask
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/end/resources/item.py
================================================
import uuid
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from schemas import ItemSchema, ItemUpdateSchema
from db import items
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
def delete(self, item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return items.values()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/end/resources/store.py
================================================
import uuid
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import stores
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(cls, store_id):
try:
# You presumably would want to include the store's items here too
# More on that when we look at databases
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
def delete(cls, store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(cls):
return stores.values()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(cls, store_data):
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/end/schemas.py
================================================
from marshmallow import Schema, fields
class ItemSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
store_id = fields.Str(required=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/start/db.py
================================================
"""
db.py
---
Later on, this file will be replaced by SQLAlchemy. For now, it mimics a database.
Our data storage is:
- stores have a unique ID and a name
- items have a unique ID, a name, a price, and a store ID.
"""
stores = {}
items = {}
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/start/requirements.txt
================================================
flask
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/start/resources/item.py
================================================
import uuid
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from schemas import ItemSchema, ItemUpdateSchema
from db import items
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
def get(self, item_id):
try:
return items[item_id]
except KeyError:
abort(404, message="Item not found.")
def delete(self, item_id):
try:
del items[item_id]
return {"message": "Item deleted."}
except KeyError:
abort(404, message="Item not found.")
@blp.arguments(ItemUpdateSchema)
def put(self, item_data, item_id):
try:
item = items[item_id]
# https://blog.teclado.com/python-dictionary-merge-update-operators/
item |= item_data
return item
except KeyError:
abort(404, message="Item not found.")
@blp.route("/item")
class ItemList(MethodView):
def get(self):
return {"items": list(items.values())}
@blp.arguments(ItemSchema)
def post(self, item_data):
for item in items.values():
if (
item_data["name"] == item["name"]
and item_data["store_id"] == item["store_id"]
):
abort(400, message=f"Item already exists.")
item_id = uuid.uuid4().hex
item = {**item_data, "id": item_id}
items[item_id] = item
return item
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/start/resources/store.py
================================================
import uuid
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from db import stores
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
def get(cls, store_id):
try:
# You presumably would want to include the store's items here too
# More on that when we look at databases
return stores[store_id]
except KeyError:
abort(404, message="Store not found.")
def delete(cls, store_id):
try:
del stores[store_id]
return {"message": "Store deleted."}
except KeyError:
abort(404, message="Store not found.")
@blp.route("/store")
class StoreList(MethodView):
def get(cls):
return {"stores": list(stores.values())}
@blp.arguments(StoreSchema)
def post(cls, store_data):
for store in stores.values():
if store_data["name"] == store["name"]:
abort(400, message=f"Store already exists.")
store_id = uuid.uuid4().hex
store = {**store_data, "id": store_id}
stores[store_id] = store
return store
================================================
FILE: docs/docs/05_flask_smorest/09_decorating_responses/start/schemas.py
================================================
from marshmallow import Schema, fields
class ItemSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
store_id = fields.Str(required=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
================================================
FILE: docs/docs/05_flask_smorest/Insomnia_section5_Docker.json
================================================
{"_type":"export","__export_format":4,"__export_date":"2022-11-09T15:36:20.139Z","__export_source":"insomnia.desktop.app:v2022.6.0","resources":[{"_id":"req_08302ba35f784bdc9fa2edc0cb080287","parentId":"fld_0bc4d91251f54e1d8e00966a259b35bc","modified":1666985452213,"created":1666905719010,"url":"{{url}}/store","name":"/store Get all store data","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423181,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_0bc4d91251f54e1d8e00966a259b35bc","parentId":"wrk_e6c8aab80c134d35810fd37d43cce51e","modified":1666905719008,"created":1666905719008,"name":"Stores","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1666124528974,"_type":"request_group"},{"_id":"wrk_e6c8aab80c134d35810fd37d43cce51e","parentId":null,"modified":1666991880304,"created":1666905718998,"name":"Section 5 - Docker","description":"","scope":"collection","_type":"workspace"},{"_id":"req_6fdedbe47a9941af9b8459816f179274","parentId":"fld_0bc4d91251f54e1d8e00966a259b35bc","modified":1666985886605,"created":1666905719013,"url":"{{url}}/store/STORE_ID","name":"/store/","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423156,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_0c240b23280746a6a1a56d7644fb89ce","parentId":"fld_0bc4d91251f54e1d8e00966a259b35bc","modified":1666987464108,"created":1666905719011,"url":"{{url}}/store","name":"/store Create new store","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My store2\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666124423143.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_fc255f6789fe45ed80b2ef83e6bb6645","parentId":"fld_0bc4d91251f54e1d8e00966a259b35bc","modified":1666985462540,"created":1666905719014,"url":"{{url}}/store/STORE_ID","name":"/store/ Delete store","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423131,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_b42e3c4d855a433394ac1a8a60c2b91b","parentId":"fld_80dee5df10c347198d8f12d85703d582","modified":1666985467338,"created":1666905719020,"url":"{{url}}/item","name":"/item Get all items","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104308,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_80dee5df10c347198d8f12d85703d582","parentId":"wrk_e6c8aab80c134d35810fd37d43cce51e","modified":1666905719016,"created":1666905719016,"name":"Items","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1666124528924,"_type":"request_group"},{"_id":"req_9a89b2ecfc61457d8cac15985597c0a0","parentId":"fld_80dee5df10c347198d8f12d85703d582","modified":1666986841489,"created":1666905719023,"url":"{{url}}/item/ITEM_ID","name":"/item/ Get item","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104258,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_3d189bf5d88349e3bce363a420407f65","parentId":"fld_80dee5df10c347198d8f12d85703d582","modified":1666987468265,"created":1666905719018,"url":"{{url}}/item","name":"/item Create item","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Chair\",\n\t\"price\": 17.99,\n\t\"store_id\": \"8efca659f8674c56b5cd035ecc0d42ec\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666125104220.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_35d865c76bce4e1b9c378d82ece413f7","parentId":"fld_80dee5df10c347198d8f12d85703d582","modified":1666985474126,"created":1666905719019,"url":"{{url}}/item/ITEM_ID","name":"/item/ Delete item","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104214.25,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_c2bf495d5cbb49d8b933b832a717662a","parentId":"fld_80dee5df10c347198d8f12d85703d582","modified":1666987071504,"created":1666905719022,"url":"{{url}}/item/ITEM_ID","name":"/item/ Update item","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Another Chair\",\n\t\"price\": 20.00\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666125104208,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_adf22718b4e044e5b54b37c869463582","parentId":"wrk_e6c8aab80c134d35810fd37d43cce51e","modified":1666985430514,"created":1666905719000,"name":"Base Environment","data":{"url":"http://127.0.0.1:5005"},"dataPropertyOrder":{"&":["url"]},"color":null,"isPrivate":false,"metaSortKey":1666122928025,"_type":"environment"},{"_id":"jar_210b7ba8709f44f29c305ed544da17c3","parentId":"wrk_e6c8aab80c134d35810fd37d43cce51e","modified":1666905719004,"created":1666905719004,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"spc_7a427f233a494727845a45ba1325ea85","parentId":"wrk_e6c8aab80c134d35810fd37d43cce51e","modified":1666905719034,"created":1666905719007,"fileName":"Flask-Smorest-Docker","contents":"","contentType":"yaml","_type":"api_spec"}]}
================================================
FILE: docs/docs/05_flask_smorest/Insomnia_section5_before_Docker.json
================================================
{"_type":"export","__export_format":4,"__export_date":"2022-11-09T15:35:47.649Z","__export_source":"insomnia.desktop.app:v2022.6.0","resources":[{"_id":"req_3d2b5cd58a4b4a6983c133118c5f8027","parentId":"fld_afac4dd2683746c586c6ff61228611de","modified":1666125193227,"created":1666124761134,"url":"http://127.0.0.1:5000/store","name":"/store Get all store data","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423181,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_afac4dd2683746c586c6ff61228611de","parentId":"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a","modified":1666125229064,"created":1666124761133,"name":"Stores","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1666124528974,"_type":"request_group"},{"_id":"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a","parentId":null,"modified":1666991873213,"created":1666124761123,"name":"Section 5 before Docker","description":"","scope":"collection","_type":"workspace"},{"_id":"req_bd3ecff11e5b49baa489812528235afb","parentId":"fld_afac4dd2683746c586c6ff61228611de","modified":1666902781180,"created":1666124761139,"url":"http://127.0.0.1:5000/store/STORE_ID","name":"/store/","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423156,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_b9dafd45675e4c478fa4dd125f4827b3","parentId":"fld_afac4dd2683746c586c6ff61228611de","modified":1666902941803,"created":1666124761136,"url":"http://127.0.0.1:5000/store","name":"/store Create new store","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My store2\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666124423143.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_34cbd59313d44bbfa4fd70166e341b05","parentId":"fld_afac4dd2683746c586c6ff61228611de","modified":1666902749338,"created":1666124977832,"url":"http://127.0.0.1:5000/store/STORE_ID","name":"/store/ Delete store","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423131,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_637d0fb6ba9d4c25b6ad9f5bdda73036","parentId":"fld_91ec9103821245f69f82aa78362f81e1","modified":1666902961406,"created":1666125038450,"url":"http://127.0.0.1:5000/item","name":"/item Get all items","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104308,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_91ec9103821245f69f82aa78362f81e1","parentId":"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a","modified":1666125224286,"created":1666124761144,"name":"Items","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1666124528924,"_type":"request_group"},{"_id":"req_e581f2420345418c84d71dbed226b6da","parentId":"fld_91ec9103821245f69f82aa78362f81e1","modified":1666125710431,"created":1666125184534,"url":"http://127.0.0.1:5000/item/ITEM_ID","name":"/item/ Get item","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104258,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_d48cf679c2664c9bb566b600634b966f","parentId":"fld_91ec9103821245f69f82aa78362f81e1","modified":1666902939274,"created":1666124761145,"url":"http://127.0.0.1:5000/item","name":"/item Create item","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Chair\",\n\t\"price\": 17.99,\n\t\"store_id\": \"f48f94a4760e40d39debf155396a9dec\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666125104220.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_8982d9bcce734f60a9f27a8eb1fc748c","parentId":"fld_91ec9103821245f69f82aa78362f81e1","modified":1666125332019,"created":1666124928966,"url":"http://127.0.0.1:5000/item/ITEM_ID","name":"/item/ Delete item","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104214.25,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_99fbb0c34cd049f1bb8ac4e944f0ae6d","parentId":"fld_91ec9103821245f69f82aa78362f81e1","modified":1666902838552,"created":1666125104208,"url":"http://127.0.0.1:5000/item/ITEM_ID","name":"/item/ Update item","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Another Chair\",\n\t\"price\": 20.00\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666125104208,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_6b3e8bb38d0c4154826d63642b863687","parentId":"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a","modified":1666124761125,"created":1666124761125,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1666122928025,"_type":"environment"},{"_id":"jar_9b95c15dadb44c03bf60cc7386095847","parentId":"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a","modified":1666124761128,"created":1666124761128,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"spc_cfb94f75feff4930966c80f350b1e115","parentId":"wrk_acbd4ac11e8a40e5be2e6dad8ec6174a","modified":1666124761155,"created":1666124761131,"fileName":"Flask-Smorest","contents":"","contentType":"yaml","_type":"api_spec"}]}
================================================
FILE: docs/docs/05_flask_smorest/_category_.json
================================================
{
"label": "Flask-Smorest for More Efficient Development",
"position": 5
}
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/01_project_overview_sqlalchemy/README.md
================================================
---
title: Project Overview, and why use SQLAlchemy
description: Let's look at what we'll do in this section. There are no changes to the client-facing API at all, just changes internally to how we store data.
ctslug: project-overview-why-use-sqlalchemy
---
# Project Overview (and why use SQLAlchemy)
:::tip Insomnia files
Remember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!
:::
In this section we'll make absolutely no changes to the API! However, we will completely change the way we store data.
Up until now, we've been storing data in an "in-memory database": a couple of Python dictionaries. When we stop the app, the data is destroyed. This is obviously not great, so we want to move to a proper store that can keep data around between app restarts!
We'll be using a relational database for data storage, and there are many different options: SQLite, MySQL, PostgreSQL, and others.
At this point we have two options regarding how to interact with the database:
1. We can write SQL code and execute it ourselves. For example, when we want to add an item to the database we'd write something like `INSERT INTO items (name, price, store_id) VALUES ("Chair", 17.99, 1)`.
2. We can use an ORM, which can take Python objects and turn them into database rows.
For this project, we are going to use an ORM because it makes the code much cleaner and simpler. Also, the ORM library (SQLAlchemy) helps us with many potential issues with using SQL, such as:
- Multi-threading support
- Handling creating the tables and defining the rows
- Database migrations (with help of another library, Alembic)
- Like mentioned, it makes the code cleaner, simpler, and shorter
To get started, add the following to the `requirements.txt` file:
```text title="requirements.txt"
sqlalchemy
flask-sqlalchemy
```
What is Flask-SQLAlchemy?
SQLAlchemy is the ORM library, that helps map Python classes to database tables and columns, and turns Python objects of those classes into specific rows.
Flask-SQLAlchemy is a Flask extension which helps connect SQLAlchemy to Flask apps.
With this, install your requirements (remember to activate your virtual environment first!).
```
pip install -r requirements.txt
```
Let's begin creating our SQLAlchemy models in the next lecture.
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/README.md
================================================
---
title: Create a simple SQLAlchemy Model
description: Lecture description goes here.
ctslug: create-a-simple-sqlalchemy-model
---
# Create a simple SQLAlchemy Model
## Initialize the SQLAlchemy instance
```python title="db.py"
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
```
## Create models without relationships
Every model inherits from `db.Model`. That way when we tell SQLAlchemy about them (in [Configure Flask-SQLAlchemy](../configure_flask_sqlalchemy))), it will know to look at them to create tables.
Every model also has a few properties that let us interact with the database through the model, such as `query` (more on this in [Insert models in the database with SQLAlchemy](../insert_models_sqlalchemy)).
```python title="models/item.py"
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(db.Integer, unique=False, nullable=False)
```
```python title="models/store.py"
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
```
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(db.Integer, unique=False, nullable=False)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
raise NotImplementedError("Getting an item is not implemented.")
def delete(self, item_id):
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
raise NotImplementedError("Creating an item is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
raise NotImplementedError("Getting a store is not implemented.")
def delete(self, store_id):
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
raise NotImplementedError("Creating a store is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/end/schemas.py
================================================
from marshmallow import Schema, fields
class ItemSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
store_id = fields.Int(required=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/requirements.txt
================================================
flask
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
raise NotImplementedError("Getting an item is not implemented.")
def delete(self, item_id):
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
raise NotImplementedError("Creating an item is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
raise NotImplementedError("Getting a store is not implemented.")
def delete(self, store_id):
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
raise NotImplementedError("Creating a store is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/02_create_simple_sqlalchemy_model/start/schemas.py
================================================
from marshmallow import Schema, fields
class ItemSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
store_id = fields.Str(required=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(Schema):
id = fields.Str(dump_only=True)
name = fields.Str(required=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/README.md
================================================
---
title: One-to-many relationships with SQLAlchemy
description: Model relationships let us easily retrieve information about a related model, without having to do SQL JOINs manually.
ctslug: one-to-many-relationships-with-sqlalchemy
---
# One-to-many relationships with SQLAlchemy
```python title="models/item.py"
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
# highlight-start
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
# highlight-end
```
```python title="models/store.py"
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
# highlight-start
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
# highlight-end
```
To make it easier to import and use the models, I'll also create a `models/__init__.py` file that imports the models from their files:
```python title="models/__init__.py"
from models.store import StoreModel
from models.item import ItemModel
```
## What is `lazy="dynamic"`?
Without `lazy="dynamic"`, the `items` attribute of the `StoreModel` resolves to a list of `ItemModel` objects.
With `lazy="dynamic"`, the `items` attribute resolves to a SQLAlchemy **query**, which has some benefits and drawbacks:
- A key benefit is load speed. Because SQLAlchemy doesn't have to go to the `items` table and load items, stores will load faster.
- A key drawback is accessing the `items` of a store isn't as easy.
- However this has another hidden benefit, which is that when you _do_ load items, you can do things like filtering before loading.
Here's how you could get all the items, giving you a list of `ItemModel` objects. Assume `store` is a `StoreModel` instance:
```python
store.items.all()
```
And here's how you would do some filtering:
```python
store.items.filter_by(name=="Chair").first()
```
## Updating our marshmallow schemas
Now that the models have these relationships, we can modify our marshmallow schemas so they will return some or all of the information about the related models.
We do this with the `Nested` marshmallow field.
:::caution
Something to be careful about is having schema A which has a nested schema B, which has a nested schema A.
This will lead to an infinite nesting, which is obviously never what you want!
:::
To avoid infinite nesting, we are renaming our schemas which _don't_ use nested fields to `Plain`, such as `PlainItemSchema` and `PlainStoreSchema`.
Then the schemas that _do_ use nesting can be called `ItemSchema` and `StoreSchema`, and they inherit from the plain schemas. This reduces duplication and prevents infinite nesting.
```python title="schemas.py"
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
```
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
raise NotImplementedError("Getting an item is not implemented.")
def delete(self, item_id):
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
raise NotImplementedError("Creating an item is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
raise NotImplementedError("Getting a store is not implemented.")
def delete(self, store_id):
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
raise NotImplementedError("Creating a store is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(db.Integer, unique=False, nullable=False)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
raise NotImplementedError("Getting an item is not implemented.")
def delete(self, item_id):
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
raise NotImplementedError("Creating an item is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
raise NotImplementedError("Getting a store is not implemented.")
def delete(self, store_id):
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
raise NotImplementedError("Creating a store is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/03_one_to_many_relationships_sqlalchemy/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/README.md
================================================
---
title: Configure Flask-SQLAlchemy
description: Link Flask-SQLAlchemy with our Flask app and create the initial tables.
ctslug: configure-flask-sqlalchemy
---
# Configure Flask-SQLAlchemy
We want to add two imports to `app.py`:
```python title="app.py"
from db import db
import models
```
## The Flask app factory pattern
Up until now, we've been creating the `app` variable (which is the Flask app) directly in `app.py`.
With the app factory pattern, we write a function that _returns_ `app`. That way we can _pass configuration values_ to the function, so that we configure the app before getting it back.
This is especially useful for testing, but also if you want to do things like have staging and production apps.
To do the app factory, all we do is place all the app-creation code inside a function which **must be called `create_app()`**.
```python title="app.py"
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
# highlight-start
def create_app():
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
# highlight-end
```
## Add Flask-SQLAlchemy code to the app factory
```python title="app.py"
import os
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
# highlight-start
def create_app(db_url=None):
# highlight-end
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
# highlight-start
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or os.getenv("DATABASE_URL", "sqlite:///data.db")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db.init_app(app)
# highlight-end
api = Api(app)
# highlight-start
with app.app_context():
db.create_all()
# highlight-end
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
```
We've done three things:
1. Added the `db_url` parameter. This lets us create an app with a certain database URL, or alternatively try to fetch the database URL from the environment variables. The default value will be a local SQLite file, if we don't pass a value ourselves and it isn't in the environment.
2. Added two SQLAlchemy values to `app.config`. One is the database URL (or URI), the other is a [configuration option](https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/) which improves performance.
3. When the app is created, tell SQLAlchemy to create all the database tables we need.
:::tip How does SQLAlchemy know what tables to create?
The line `import models` lets SQLAlchemy know what models exist in our application. Because they are `db.Model` instances, SQLAlchemy will look at their `__tablename__` and defined `db.Column` attributes to create the tables.
:::
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
raise NotImplementedError("Getting an item is not implemented.")
def delete(self, item_id):
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
raise NotImplementedError("Creating an item is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
raise NotImplementedError("Getting a store is not implemented.")
def delete(self, store_id):
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
raise NotImplementedError("Creating a store is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
api = Api(app)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
raise NotImplementedError("Getting an item is not implemented.")
def delete(self, item_id):
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
raise NotImplementedError("Creating an item is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
raise NotImplementedError("Getting a store is not implemented.")
def delete(self, store_id):
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
raise NotImplementedError("Creating a store is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/04_configure_flask_sqlalchemy/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/README.md
================================================
---
title: Insert models in the database with SQLAlchemy
description: Learn how to use SQLAlchemy to add new rows to our SQL database.
ctslug: insert-models-in-database-with-sqlalchemy
---
# Insert models in the database with SQLAlchemy
Inserting models with SQLAlchemy couldn't be easier! We'll use the `db.session`[^1] variable to `.add()` a model. Let's begin working on our `Item` resource:
```python title="resources/item.py"
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
...
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
```
Similarly in our `Store` resource:
```python title="resources/store.py"
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
...
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
```
Note here we're catching two different errors, `IntegrityError` for when a client attempts to create a store with a name that already exists, and `SQLAlchemyError` for anything else.
Since the `StoreModel`'s `name` column is marked as `unique=True`, then an `IntegrityError` is raised when we try to insert another row with the same name.
[^1]: [Session Basics (SQLAlchemy Documentation)](https://docs.sqlalchemy.org/en/14/orm/session_basics.html)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
raise NotImplementedError("Getting an item is not implemented.")
def delete(self, item_id):
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
raise NotImplementedError("Getting a store is not implemented.")
def delete(self, store_id):
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
raise NotImplementedError("Getting an item is not implemented.")
def delete(self, item_id):
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
raise NotImplementedError("Creating an item is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
raise NotImplementedError("Getting a store is not implemented.")
def delete(self, store_id):
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
raise NotImplementedError("Creating a store is not implemented.")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/05_insert_models_sqlalchemy/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/README.md
================================================
---
title: Get models by ID from the database using SQLAlchemy
description: Learn how to fetch a specific model using its primary key column, and how to return a 404 page if it isn't found.
ctslug: get-models-by-id-from-the-database
---
# Get models by ID from the database using SQLAlchemy
Using the model class's `query` attribute, we have access to two very handy methods:
- `ItemModel.query.get(item_id)` gives us an `ItemModel` object from the database where the `item_id` matches the primary key.
- `ItemModel.query.get_or_404(item_id)` does the same, but makes Flask immediately return a "Not Found" message, together with a 404 error code, if no model can be found with that ID in the database.
:::tip
When we use `.get_or_404()` and nothing is found, this is the response from the API:
```json
{"code": 404, "status": "Not Found"}
```
The status code of this response is also 404.
:::
We're going to use `.get_or_404()` repeatedly in our resources!
For now, and since we'll need an `ItemModel` instance in all our `Item` resource methods, let's add that:
```python title="resources/item.py"
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
# highlight-start
item = ItemModel.query.get_or_404(item_id)
return item
# highlight-end
def delete(self, item_id):
# highlight-start
item = ItemModel.query.get_or_404(item_id)
# highlight-end
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
# highlight-start
item = ItemModel.query.get_or_404(item_id)
# highlight-end
raise NotImplementedError("Updating an item is not implemented.")
```
Similarly in our `Store` resource:
```python title="resources/store.py"
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
# highlight-start
store = StoreModel.query.get_or_404(store_id)
return store
# highlight-end
def delete(self, store_id):
# highlight-start
store = StoreModel.query.get_or_404(store_id)
# highlight-end
raise NotImplementedError("Deleting a store is not implemented.")
```
With this, we're ready to continue!
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
raise NotImplementedError("Getting an item is not implemented.")
def delete(self, item_id):
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
raise NotImplementedError("Getting a store is not implemented.")
def delete(self, store_id):
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/06_get_models_or_404/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/README.md
================================================
---
title: Updating models with SQLAlchemy
description: How to make changes to an existing model, or insert one if it doesn't already exist.
ctslug: updating-models-with-sqlalchemy
---
# Updating models with SQLAlchemy
A frequent operation in REST APIs is the "upsert", or "update or insert".
This is an idempotent operation where we send the data we want the API to store. If the data identifier already exists, an update is done. If it doesn't, it is created.
This idempotency is frequently seen with `PUT` requests. You can see it in action here:
```python title="resources/item.py"
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
# highlight-start
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
# highlight-end
```
Our `ItemUpdateSchema` at the moment looks like this:
```python title="schemas.py"
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
```
But since now our update endpoint may create items, we need to change the schema to optionally accept a `store_id`.
When updating an item, `name` or `price` (or both) may be passed, but when creating an item, `name`, `price`, and `store_id` must be passed.
Update the `ItemUpdateSchema` to this:
```python title="schemas.py"
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
store_id = fields.Int()
```
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
raise NotImplementedError("Updating an item is not implemented.")
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/07_updating_models_sqlalchemy/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/README.md
================================================
---
title: Retrieve a list of all models
description: Get more than one model and return it as a list from the API.
ctslug: retrieve-a-list-of-all-models
---
# Retrieve a list of all models
Using the `query` attribute of our model class, we can retrieve all the results of the query:
```python title="resources/item.py"
@blp.response(200, ItemSchema(many=True))
def get(self):
# highlight-start
return ItemModel.query.all()
# highlight-end
```
```python title="resources/store.py"
@blp.response(200, StoreSchema(many=True))
def get(self):
# highlight-start
return StoreModel.query.all()
# highlight-end
```
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing items is not implemented.")
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema, ItemSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
raise NotImplementedError("Listing stores is not implemented.")
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/08_retrieve_list_all_models/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/README.md
================================================
---
title: Delete models with SQLAlchemy
description: Use SQLAlchemy to handle removal of a specific model.
ctslug: delete-models-with-sqlalchemy
---
# Delete models with SQLAlchemy
Just as with adding, deleting models is a matter of using `db.session`, and then committing when the deletion is complete:
```python title="resources/item.py"
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
# highlight-start
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
# highlight-end
```
```python title="resources/store.py"
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
# highlight-start
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
# highlight-end
```
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/models/__init__.py
================================================
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
raise NotImplementedError("Deleting an item is not implemented.")
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
raise NotImplementedError("Deleting a store is not implemented.")
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/09_delete_models_sqlalchemy/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/README.md
================================================
---
title: Delete models with relationships using cascades
description: Tell SQLAlchemy what to do with related models when you delete the parent.
ctslug: delete-models-with-relationships-using-cascades
---
# Delete models with relationships using cascades
When you delete a model that has a relationship to other models that still exist, the default behavior in SQLAlchemy with PostgreSQL is to raise an error. This is because SQLAlchemy does not want to allow you to accidentally delete data that is still being used by other models.
Let's say you have a `Store 1` that has two items, `Item 1` and `Item 2`. If you try to delete Store 1 without first deleting Item 1 and Item 2, SQLAlchemy will raise an error because the items are still related to the store.
This means the items have a **Foreign Key** that references the store you're trying to delete. If the store actually was deleted, the items have a store ID that references something that doesn't exist.
To fix this, you can use a feature called "cascading deletes". Cascading deletes allow you to specify that when a model is deleted, any related models should also be deleted automatically.
SQLAlchemy makes it easy to add cascades to our models, here's how you might do that!
```python title="models/store.py"
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
# highlight-start
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic", cascade="all, delete")
# highlight-end
```
Remember that `StoreModel` and `ItemModel` have a one-to-many relationship, where each store can have multiple items, and each item belongs to a single store.
The `cascade="all,delete"` argument in the `relationship()` call for the `StoreModel.items` attribute specifies that when a store is deleted, all of its related items should also be deleted.
If you add a `cascade` on the relationship in the `ItemModel`, then when an item is deleted, its related store should also be deleted. This is not what we want, so we won't add a cascade to `ItemModel`.
With this code in place, if you try to delete a store that still has items, the items will be deleted automatically along with the store. This will allow you to delete the store without having to delete the items individually.
For more information, I strongly recommend reading [the official documentation](https://docs.sqlalchemy.org/en/20/orm/cascades.html#delete)! There are also other cascade options you can pass in depending on what you want to happen to related models when the parent changes or is deleted.
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship(
"ItemModel", back_populates="store", lazy="dynamic", cascade="all, delete"
)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
import models
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/models/__init__.py
================================================
from models.item import ItemModel
from models.store import StoreModel
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/requirements.txt
================================================
flask
flask-sqlalchemy
flask-smorest
python-dotenv
marshmallow
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/10_delete_related_models_sqlalchemy/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/11_conclusion/README.md
================================================
---
title: Conclusion of this section
description: Review everything we've changed this section to add SQL storage with SQLAlchemy to our API.
ctslug: conclusion-of-this-section
---
# Conclusion of this section
Adding SQL storage to our app has required quite a few changes! Let's do a quick review.
## Installed SQLAlchemy and Flask-SQLAlchemy
```
pip install sqlalchemy flask-sqlalchemy
```
And
```text title="requirements.txt"
sqlalchemy
flask-sqlalchemy
```
## Created models
```python title="models/item.py"
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
```
And
```python title="models/store.py"
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
```
## Updated resources to use SQLAlchemy
Previously we were using Python dictionaries as a database. Now we've swapped them out for using SQLAlchemy models by:
- Importing the models in our resource files
- Retrieving models from the database with `ModelClass.query.get_or_404(model_id)`.
- Updating models by changing attributes, or creating new model class instances, and then saving and committing with `db.session.add(model_instance)` and `db.session.commit()`.
- Deleting models with `db.session.delete(model_instance)` followed by `db.session.commit()`.
## Updated marshmallow schemas
Since now our models have relationships, that means that the schemas can have `Nested` fields.
The schemas that don't have `Nested` fields we've called "Plain" schemas, and those that do are named after the model they represent.
```python title="schemas.py"
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
store_id = fields.Int()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
```
And that's it! Quite a few changes, but hopefully you're still with me.
In the following sections we'll be adding more functionality to our API, so stay tuned!
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/Insomnia_section6.json
================================================
{"_type":"export","__export_format":4,"__export_date":"2022-11-09T15:30:25.805Z","__export_source":"insomnia.desktop.app:v2022.6.0","resources":[{"_id":"req_8612530e54144a039af84006ee8c882d","parentId":"fld_7ed8d16fd87545519f2f64b2613ea84a","modified":1666987689179,"created":1666987689179,"url":"{{url}}/store","name":"/store Get all store data","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423181,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_7ed8d16fd87545519f2f64b2613ea84a","parentId":"wrk_a6cd641e98494bca9a11fe77b66c7e37","modified":1666987689178,"created":1666987689178,"name":"Stores","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1666124528974,"_type":"request_group"},{"_id":"wrk_a6cd641e98494bca9a11fe77b66c7e37","parentId":null,"modified":1666987689171,"created":1666987689171,"name":"Section 6","description":"","scope":"collection","_type":"workspace"},{"_id":"req_335002433e9745068d074f1f942ddde2","parentId":"fld_7ed8d16fd87545519f2f64b2613ea84a","modified":1666987689183,"created":1666987689183,"url":"{{url}}/store/STORE_ID","name":"/store/","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423156,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_4f7b9d616b0e44ca94ca51cc71660da0","parentId":"fld_7ed8d16fd87545519f2f64b2613ea84a","modified":1666990320166,"created":1666987689181,"url":"{{url}}/store","name":"/store Create new store","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My store2\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666124423143.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_9228903cf7a54601a51a59f6a6692363","parentId":"fld_7ed8d16fd87545519f2f64b2613ea84a","modified":1666987689184,"created":1666987689184,"url":"{{url}}/store/STORE_ID","name":"/store/ Delete store","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423131,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_d1d499ead63e469ca04571899cc4759f","parentId":"fld_8761c7b0aa5142cba8985868cbda3de2","modified":1666987689190,"created":1666987689190,"url":"{{url}}/item","name":"/item Get all items","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104308,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_8761c7b0aa5142cba8985868cbda3de2","parentId":"wrk_a6cd641e98494bca9a11fe77b66c7e37","modified":1666987689186,"created":1666987689186,"name":"Items","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1666124528924,"_type":"request_group"},{"_id":"req_94738c7e8c774bd597ffe97bf7b921b6","parentId":"fld_8761c7b0aa5142cba8985868cbda3de2","modified":1666987689194,"created":1666987689194,"url":"{{url}}/item/ITEM_ID","name":"/item/ Get item","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104258,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_0624b67ef6b841f482b7e7522fb6f405","parentId":"fld_8761c7b0aa5142cba8985868cbda3de2","modified":1666990328367,"created":1666987689187,"url":"{{url}}/item","name":"/item Create item","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Chair\",\n\t\"price\": 17.99,\n\t\"store_id\": 1\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666125104220.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_b2651043ea5e4b33b073f260712fb114","parentId":"fld_8761c7b0aa5142cba8985868cbda3de2","modified":1666987689189,"created":1666987689189,"url":"{{url}}/item/ITEM_ID","name":"/item/ Delete item","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104214.25,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_640f313dbd5a4bfcbf98081e2fab6d4a","parentId":"fld_8761c7b0aa5142cba8985868cbda3de2","modified":1666987689192,"created":1666987689192,"url":"{{url}}/item/ITEM_ID","name":"/item/ Update item","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Another Chair\",\n\t\"price\": 20.00\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666125104208,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_892efa21f8454221972d0c77a336872c","parentId":"wrk_a6cd641e98494bca9a11fe77b66c7e37","modified":1666987689172,"created":1666987689172,"name":"Base Environment","data":{"url":"http://127.0.0.1:5005"},"dataPropertyOrder":{"&":["url"]},"color":null,"isPrivate":false,"metaSortKey":1666122928025,"_type":"environment"},{"_id":"jar_aff586a35c4c49aa91c5defb067355bf","parentId":"wrk_a6cd641e98494bca9a11fe77b66c7e37","modified":1666987689174,"created":1666987689174,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"spc_4e7424f78749436bacdb44d3a1eba77a","parentId":"wrk_a6cd641e98494bca9a11fe77b66c7e37","modified":1666987689205,"created":1666987689176,"fileName":"Section 6","contents":"","contentType":"yaml","_type":"api_spec"}]}
================================================
FILE: docs/docs/06_sql_storage_sqlalchemy/_category_.json
================================================
{
"label": "SQL Storage with Flask-SQLAlchemy",
"position": 6
}
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/01_section_changes/README.md
================================================
---
title: Changes in this section
description: In this section we add Tags to our Stores, and link these to Items using a many-to-many relationship.
ctslug: changes-in-many-to-many-section
---
# Changes in this section
:::tip Insomnia files
Remember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!
:::
It's common for online stores to use "tags" to group items and to be able to search for them a bit more easily.
For example, an item "Chair" could be tagged with "Furniture" and "Office".
Another item, "Laptop", could be tagged with "Tech" and "Office".
So one item can be associated with many tags, and one tag can be associated with many items.
This is a many-to-many relationship, which is bit trickier to implement than the one-to-many we've already implemented between Items and Stores.
## When you have many stores
We want to add one more constraint to tags, however. That is that if we have many stores, it's possible each store wants to use different tags. So the tags we create will be unique to each store.
This means that tags will have:
- A many-to-one relationship with stores
- A many-to-many relationship with items
Here's a diagram to illustrate what this looks like:

## New API endpoints to be added
In this section we will add all the Tag endpoints:
| Method | Endpoint | Description |
| -------- | --------------------- | ------------------------------------------------------- |
| `GET` | `/store/{id}/tag` | Get a list of tags in a store. |
| `POST` | `/store/{id}/tag` | Create a new tag. |
| `POST` | `/item/{id}/tag/{id}` | Link an item in a store with a tag from the same store. |
| `DELETE` | `/item/{id}/tag/{id}` | Unlink a tag from an item. |
| `GET` | `/tag/{id}` | Get information about a tag given its unique id. |
| `DELETE` | `/tag/{id}` | Delete a tag, which must have no associated items. |
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/README.md
================================================
---
title: One-to-many relationships review
description: A super-quick look at creating the Tag model and setting up the one-to-many relationship with Stores.
ctslug: one-to-many-relationship-between-tag-store
---
# One-to-many relationship between Tag and Store
Since we've already learned how to set up one-to-many relationships with SQLAlchemy when we looked at Items and Stores, let's go quickly in this section.
## The SQLAlchemy models
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
```python title="models/tag.py"
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
```
```python title="models/store.py"
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
# highlight-start
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
# highlight-end
```
Remember to import the `TagModel` in `models/__init__.py` so that it is then imported by `app.py`. Otherwise SQLAlchemy won't know about it, and it won't be able to create the tables.
## The marshmallow schemas
These are the new schemas we'll add. Note that none of the tag schemas have any notion of "items". We'll add those to the schemas when we construct the many-to-many relationship.
In the `StoreSchema` we add a new list field for the nested `PlainTagSchema`, just as it has with `PlainItemSchema`.
```python title="schemas.py"
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
# highlight-start
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
# highlight-end
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
```
## The API endpoints
Let's add the Tag endpoints that aren't related to Items:
| Method | Endpoint | Description |
| ---------- | --------------------- | ------------------------------------------------------- |
| ✅ `GET` | `/store/{id}/tag` | Get a list of tags in a store. |
| ✅ `POST` | `/store/{id}/tag` | Create a new tag. |
| ❌ `POST` | `/item/{id}/tag/{id}` | Link an item in a store with a tag from the same store. |
| ❌ `DELETE` | `/item/{id}/tag/{id}` | Unlink a tag from an item. |
| ✅ `GET` | `/tag/{id}` | Get information about a tag given its unique id. |
| ❌ `DELETE` | `/tag/{id}` | Delete a tag, which must have no associated items. |
Here's the code we need to write to add these endpoints:
```python title="resources/tag.py"
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel
from schemas import TagSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all()
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
```
## Register the Tag blueprint in `app.py`
Finally, we need to remember to import the blueprint and register it!
```python title="app.py"
from flask import Flask
from flask_smorest import Api
import models
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
# highlight-start
from resources.tag import blp as TagBlueprint
# highlight-end
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
# highlight-start
api.register_blueprint(TagBlueprint)
# highlight-end
return app
```
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
import models
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/models/__init__.py
================================================
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel
from schemas import TagSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
import models
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
return app
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/models/__init__.py
================================================
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/README.md
================================================
---
title: Many-to-many relationships
description: Learn to set up a many-to-many relationship between two models using SQLAlchemy.
ctslug: many-to-many-relationships
---
# Many-to-many relationships
## The SQLAlchemy models
In one-to-many relationships, one of the models has a foreign key that links it to another model.
However, for a many-to-many relationship, one model can't have a single value as a foreign key (otherwise it would be a one-to-many!). Instead, what we do is construct a **secondary table** that has, in each row, a tag ID and and item ID.
| id | tag_id | item_id |
| --- | ------ | ------- |
| 1 | 2 | 5 |
| 2 | 1 | 4 |
| 3 | 4 | 5 |
| 4 | 1 | 3 |
Explanation of the table above
The table above has 4 rows, which tell us the following:
- Tag with ID
1 is linked to Items with IDs 3 and 4.
- Tag with ID
2 is linked to Item with ID 5.
- Tag with ID
4 is linked to Item with ID 5.
And therefore:
- Item with ID
3 is linked to Tag with ID 1.
- Item with ID
4 is linked to Tag with ID 1.
- Item with ID
5 is linked to Tags with IDs 2 and 4.
This is how many-to-many relationships work, and through this secondary table, the Tag.items and Item.tags attributes will be populated by SQLAlchemy.
The rows in this table then signify a link between a specific tag and a specific item, but without the need for those values to be stored in the tag or item models themselves.
### Writing the secondary table for many-to-many relationships
As we've just seen, many-to-many relationships use a secondary table which stores which models of one side are related to which models of the other side.
Just as we did with `Item`, `Store`, and `Tag`, we'll create a model for this secondary table:
```python title="models/item_tags.py"
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
```
Let's also add this to our `models/__init__.py` file:
```python title="models/__init__.py"
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
```
### Using the secondary table in the main models
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
```python title="models/tag.py"
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
# highlight-start
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
# highlight-end
```
```python title="models/item.py"
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
# highlight-start
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
# highlight-end
```
## The marshmallow schemas
Next up, let's add the nested fields to the marshmallow schemas.
The `TagAndItemSchema` will be used to return information about both the Item and Tag that have been modified in an endpoint, together with an informative message.
```python title="schemas.py"
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
# highlight-start
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
# highlight-end
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
# highlight-start
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
# highlight-end
store = fields.Nested(PlainStoreSchema(), dump_only=True)
# highlight-start
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
# highlight-end
```
## The API endpoints
Now let's add the rest of our API endpoints (grayed out are the ones we implemented in [one-to-many relationships review](../one_to_many_review/))!
| Method | Endpoint | Description |
| ---------------------------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| ✅ `GET` | `/store/{id}/tag` | Get a list of tags in a store. |
| ✅ `POST` | `/store/{id}/tag` | Create a new tag. |
| ✅ `POST` | `/item/{id}/tag/{id}` | Link an item in a store with a tag from the same store. |
| ✅ `DELETE` | `/item/{id}/tag/{id}` | Unlink a tag from an item. |
| ✅ `GET` | `/tag/{id}` | Get information about a tag given its unique id. |
| ✅ `DELETE` | `/tag/{id}` | Delete a tag, which must have no associated items. |
Here's the code (new lines highlighted):
```python title="resources/tag.py"
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
# highlight-start
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
# highlight-end
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
# highlight-start
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
# highlight-end
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
# highlight-start
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.",
)
# highlight-end
```
And with that, we're done!
## Making sure Store ID matches when linking tags
If you wanted to, you can make sure that you can only link a tag that belongs to a certain store, with an item of that same store.
Something like this would work:
```py
if item.store.id != tag.store.id:
abort(400, message="Make sure item and tag belong to the same store before linking.")
```
Now we're ready to look at securing API endpoints with user authentication.
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
import models
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/conftest.py
================================================
import pytest
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/models/__init__.py
================================================
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
import models
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/models/__init__.py
================================================
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel
from schemas import TagSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/03_many_to_many_relationships/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/Insomnia_section7.json
================================================
{"_type":"export","__export_format":4,"__export_date":"2022-11-09T15:30:50.558Z","__export_source":"insomnia.desktop.app:v2022.6.0","resources":[{"_id":"req_379d0e42420f466bbad1b7481e5e7816","parentId":"fld_86b5e8072a894c409febe46716e99809","modified":1666991794866,"created":1666990973919,"url":"{{url}}/store/STORE_ID/tag","name":"/stores//tags Get tags in store","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666990973919,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_86b5e8072a894c409febe46716e99809","parentId":"wrk_6efa5c8b8fa142a28f436b209fba66fa","modified":1666990939045,"created":1666990939045,"name":"Tags","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1666990939045,"_type":"request_group"},{"_id":"wrk_6efa5c8b8fa142a28f436b209fba66fa","parentId":null,"modified":1666990745588,"created":1666990745588,"name":"Section 7","description":"","scope":"collection","_type":"workspace"},{"_id":"req_85adfd198935497bb7aedb266beb5bf3","parentId":"fld_86b5e8072a894c409febe46716e99809","modified":1666991788350,"created":1666990945502,"url":"{{url}}/tag/TAG_ID","name":"/tags/ Get tag","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666990945502,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_4765f7ca8e1e46308cdde255d09a2ffc","parentId":"fld_86b5e8072a894c409febe46716e99809","modified":1666991810641,"created":1666991378432,"url":"{{url}}/item/ITEM_ID/tag/TAG_ID","name":"/item//tag/ Link an item in a store with a tag from the same store","description":"","method":"POST","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666990945477,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_f07aab6ead044ca7bba0de3437ab08c4","parentId":"fld_86b5e8072a894c409febe46716e99809","modified":1666991779049,"created":1666991031108,"url":"{{url}}/store/STORE_ID/tag","name":"/stores//tags Create tag in store","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Tag name\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666990945452,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_77d1a5f225c54acbb27bac15010722ad","parentId":"fld_86b5e8072a894c409febe46716e99809","modified":1666991824192,"created":1666991489163,"url":"{{url}}/item/ITEM_ID/tag/TAG_ID","name":"/item//tag/ Unlink a tag from an item","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666990945427,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_d60510ab22b2499abb20a63629e30fcd","parentId":"fld_86b5e8072a894c409febe46716e99809","modified":1666991828682,"created":1666991524256,"url":"{{url}}/tag/TAG_ID","name":"/tag/ Delete a tag, which must have no associated items.","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666990945402,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_ab3c728a796e4b4ca51803248e1b0650","parentId":"fld_597937a09435404ebe2200cbaeed101d","modified":1666990745596,"created":1666990745596,"url":"{{url}}/store","name":"/store Get all store data","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423181,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_597937a09435404ebe2200cbaeed101d","parentId":"wrk_6efa5c8b8fa142a28f436b209fba66fa","modified":1666990745596,"created":1666990745596,"name":"Stores","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1666124528974,"_type":"request_group"},{"_id":"req_a9d43bb23e1246da94aec50b9b9ca652","parentId":"fld_597937a09435404ebe2200cbaeed101d","modified":1666990745601,"created":1666990745601,"url":"{{url}}/store/STORE_ID","name":"/store/","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423156,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_8a36225a08bb4dfbbf98fd983b0d4a5f","parentId":"fld_597937a09435404ebe2200cbaeed101d","modified":1666991654175,"created":1666990745599,"url":"{{url}}/store","name":"/store Create new store","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"My store2\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666124423143.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_303507538c0f408eb6d91784b7ed8d36","parentId":"fld_597937a09435404ebe2200cbaeed101d","modified":1666990745602,"created":1666990745602,"url":"{{url}}/store/STORE_ID","name":"/store/ Delete store","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666124423131,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_f4776751aecc4c6eafb264dc2d2c24cb","parentId":"fld_baa111a1ff5849b4838637f09844bfde","modified":1666990745609,"created":1666990745609,"url":"{{url}}/item","name":"/item Get all items","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104308,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_baa111a1ff5849b4838637f09844bfde","parentId":"wrk_6efa5c8b8fa142a28f436b209fba66fa","modified":1666990745604,"created":1666990745604,"name":"Items","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1666124528924,"_type":"request_group"},{"_id":"req_e6bc2422c8cf4f119c7dc10251a9af65","parentId":"fld_baa111a1ff5849b4838637f09844bfde","modified":1666990745611,"created":1666990745611,"url":"{{url}}/item/ITEM_ID","name":"/item/ Get item","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104258,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_6c72c92f81924ce7bc26ceb488fd64ff","parentId":"fld_baa111a1ff5849b4838637f09844bfde","modified":1666991658886,"created":1666990745605,"url":"{{url}}/item","name":"/item Create item","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Chair\",\n\t\"price\": 17.99,\n\t\"store_id\": 1\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666125104220.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_e86e0877045640d690454a99b176f3a2","parentId":"fld_baa111a1ff5849b4838637f09844bfde","modified":1666990745607,"created":1666990745607,"url":"{{url}}/item/ITEM_ID","name":"/item/ Delete item","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1666125104214.25,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_f0c4a3d747a543249131e19ceea79e56","parentId":"fld_baa111a1ff5849b4838637f09844bfde","modified":1666990745610,"created":1666990745610,"url":"{{url}}/item/ITEM_ID","name":"/item/ Update item","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Another Chair\",\n\t\"price\": 20.00\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1666125104208,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_7609e8f1315a4d77af52a6ba50f48205","parentId":"wrk_6efa5c8b8fa142a28f436b209fba66fa","modified":1666990745590,"created":1666990745590,"name":"Base Environment","data":{"url":"http://127.0.0.1:5005"},"dataPropertyOrder":{"&":["url"]},"color":null,"isPrivate":false,"metaSortKey":1666122928025,"_type":"environment"},{"_id":"jar_ce9759718e054191a685cec521ed7afc","parentId":"wrk_6efa5c8b8fa142a28f436b209fba66fa","modified":1666990745592,"created":1666990745592,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"spc_0f62897a05a449f9845b4c71eeb892b3","parentId":"wrk_6efa5c8b8fa142a28f436b209fba66fa","modified":1666990745620,"created":1666990745594,"fileName":"Section 7","contents":"","contentType":"yaml","_type":"api_spec"}]}
================================================
FILE: docs/docs/07_sqlalchemy_many_to_many/_category_.json
================================================
{
"label": "Many-to-many relationships with SQLAlchemy",
"position": 7
}
================================================
FILE: docs/docs/08_flask_jwt_extended/01_section_changes/README.md
================================================
---
title: Changes in this section
description: Overview of the API endpoints we'll use for user registration and authentication.
ctslug: changes-in-jwt-extended-section
---
# Changes in this section
:::tip Insomnia files
Remember to get the Insomnia files for this section or for all sections [here](/insomnia-files/)!
:::
In this section we will add the following endpoints:
| Method | Endpoint | Description |
| -------------- | ----------------- | ----------------------------------------------------- |
| `POST` | `/register` | Create user accounts given an `email` and `password`. |
| `POST` | `/login` | Get a JWT given an `email` and `password`. |
| 🔒
`POST` | `/logout` | Revoke a JWT. |
| 🔒
`POST` | `/refresh` | Get a fresh JWT given a refresh JWT. |
| `GET` | `/user/{user_id}` | (dev-only) Get info about a user given their ID. |
| `DELETE` | `/user/{user_id}` | (dev-only) Delete a user given their ID. |
We will also protect some existing endpoints by requiring a JWT from clients. You can see which endpoints will be protected in [The API we'll build in this course](/docs/course_intro/what_is_rest_api/#the-api-well-build-in-this-course)
================================================
FILE: docs/docs/08_flask_jwt_extended/02_what_is_a_jwt/README.md
================================================
---
title: What is a JWT?
description: Understand what a JWT is, what data it contains, and how it may be used.
ctslug: what-is-a-jwt
---
# What is a JWT?
A JWT is a signed JSON object with a specific structure. Our Flask app will sign the JWTs with the secret key, proving that _it generated them_.
The Flask app generates a JWT when a user logs in (with their username and password). In the JWT, we'll store the user ID. The client then stores the JWT and sends it to us on every request.
Because we can prove our app generated the JWT (through its signature), and we will receive the JWT with the user ID in every request, we can _treat requests that include a JWT as "logged in"_.
For example, if we want certain endpoints to only be accessible to logged-in users, all we do is require a JWT in them. Since the client can only get a JWT after logging in, we know that including a JWT is proof that the client logged in successfully at some point in the past.
And since the JWT includes the user ID inside it, when we receive a JWT we know _who logged in_ to get the JWT.
There's a lot more information about JWTs here: [https://jwt.io/introduction](https://jwt.io/introduction). This includes information such as:
- What is stored inside a JWT?
- Are JWTs secure?
================================================
FILE: docs/docs/08_flask_jwt_extended/03_how_is_jwt_used/README.md
================================================
---
title: How are JWTs used?
description: Learn who uses JWTs and how they are used by clients and servers to perform authentication.
ctslug: how-are-jwts-used
---
# How are JWTs used?
:::info JWT vs. Access token?
An "access token" is any piece of information that a client can use to authenticate. In this API, we use JWTs. Therefore you can say that the JWT and the access token are one and the same!
:::
We've learned that a JWT is generated by the API and sent to the client. When the client wants to login they will send the API information that allows them to do so: usually, the user's username and password. The API then validates that this login information is correct, and generates the access token.
Inside the access token, the API stores identifying information for the user. Then the access token is sent to the client who stores it in whichever way they see fit. In every subsequent request to the API, the client should include the access token. That way, just with that information, the API can tell _who_ made the request. The API can decode the access token and see inside it the identifying information for the user for whom the access token was generated.
Here is a diagram of the interaction between client and API to generate an access token:

## An example of using access tokens
For example, let's say you want to make an API that has an endpoint `/my-info`. This endpoint should send the client information about the currently logged-in user.
Let's imagine that **the client** is a website. In the website, there is a button, "See my info", which when clicked sends a request to the API's `/my-info` endpoint to get the logged-in user's information.
### Clicking the button without logging in
If the user navigates to the website and clicks the "See my info" button, the website will send a request to the API. Because the user hasn't logged in yet, the website doesn't have an access token generated for this user.
Therefore, the API responds with an "authentication error".
The website receives the authentication error and that tells it that the user hasn't logged in. So the website can show the user a log-in form, for the user to enter their username and password.
When the user enters their username and password, the website will send a request to the API's `/login` endpoint. The API then responds with the access token. The website stores the access token for use later.
If the user clicks the "See my info" button again, now the website will include the access token in the request.
The server will then:
1. See the access token.
2. Decode it.
3. Look at what user the access token was generated for.
4. Load _that_ user's information from the database.
5. Respond with that user's information.
The website receives the user's information, and can display it.
This is why the user sees their own information, and not someone else's. The access token was generated after they logged in with their details, and the access token stores their user ID. The server will use that to retrieve the correct data.
Here is a rather long diagram depicting what happens:

:::warning This course deals only with the API
Remember that in this course, we're making the API. We are not concerned with the client! We don't care how the client stores the access token or even whether the client is a website, mobile app, Postman or Insomnia, or anything else!
:::
## When do users provide their username and password?
Access tokens don't last forever: they normally have expiry times within 30 days of being generated. The shorter the expiry time of an access token, the more often that the user has to re-authenticate by providing their username and password, but the more secure the token is.
Tokens are more secure if they expire sooner because if the user forgets to log out of a shared device, and someone else tries to use their account, the token will expire and they will be unable to use the account.
Obviously, it's not a great experience for users if they have to keep re-entering their username and password constantly. Towards the end of this section we will learn about [token refreshing](../12_token_refreshing_flask_jwt_extended/README.md), which is a way to reduce the amount of times users have to re-authenticate, without affecting security too much.
================================================
FILE: docs/docs/08_flask_jwt_extended/03_how_is_jwt_used/how-are-jwts-used.key
================================================
[File too large to display: 17.2 MB]
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/README.md
================================================
---
title: Flask-JWT-Extended setup
description: Install and set up the Flask-JWT-Extended extension with our REST API.
ctslug: flask-jwt-extended-setup
---
# Flask-JWT-Extended setup
First, let's update our requirements:
```diff title="requirements.txt"
+ flask-jwt-extended
```
Then we must do two things:
- Add the extension to our `app.py`.
- Set a secret key that the extension will use to _sign_ the JWTs.
```python title="app.py"
from flask import Flask
from flask_smorest import Api
# highlight-start
from flask_jwt_extended import JWTManager
# highlight-end
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
# highlight-start
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
# highlight-end
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
```
:::caution
The secret key set here, `"jose"`, is **not very safe**.
Instead you should generate a long and random secret key using something like `str(secrets.SystemRandom().getrandbits(128))`.
:::
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/conftest.py
================================================
import pytest
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/models/__init__.py
================================================
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship(
"ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
)
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client):
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client):
response = client.get(
"/item/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/conftest.py
================================================
import pytest
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/models/__init__.py
================================================
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship(
"ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
)
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client):
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client):
response = client.get(
"/item/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/04_flask_jwt_extended_setup/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/README.md
================================================
---
title: The User model and schema
description: Create the SQLAlchemy User model and marshmallow schema.
ctslug: the-user-model-and-schema
---
# The User model and schema
Just as we did with items, stores, and tags, let's create two classes for our users:
- The SQLAlchemy model, to interact with the database.
- The marshmallow schema, to deserialize data from clients and serialize it back to return data.
## The User SQLAlchemy model
```python title="models/user.py"
from db import db
class UserModel(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
```
Let's also add this class to `models/__init__.py` so it can then be imported by `app.py`:
```python title="models/__init__.py"
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
```
## The User marshmallow schema
```python title="schemas.py"
class UserSchema(Schema):
id = fields.Int(dump_only=True)
username = fields.Str(required=True)
password = fields.Str(required=True, load_only=True)
```
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/conftest.py
================================================
import pytest
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/__init__.py
================================================
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship(
"ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/models/user.py
================================================
from db import db
class UserModel(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
)
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client):
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client):
response = client.get(
"/item/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
class UserSchema(Schema):
id = fields.Int(dump_only=True)
username = fields.Str(required=True)
password = fields.Str(required=True, load_only=True)
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/conftest.py
================================================
import pytest
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/models/__init__.py
================================================
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
)
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client):
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client):
response = client.get(
"/item/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/05_user_model_and_schema/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/README.md
================================================
---
title: How to add a register endpoint to the REST API
description: Learn how to add a registration endpoint to a REST API using Flask-Smorest and Flask-JWT-Extended.
ctslug: how-to-add-a-register-endpoint-to-the-rest-api
---
# How to add a register endpoint to the REST API
Registering users sounds like a conceptually very difficult thing, but let's break it down into steps:
- Receive username and password from the client (as JSON).
- Check if a user with that username already exists.
- If it doesn't...
- Encrypt the password.
- Add a new `UserModel` to the database.
- Return a success message.
## Boilerplate set-up for a blueprint with Flask-Smorest
First, we need our imports and blueprint set-up. This is the same for pretty much every Flask-Smorest blueprint, so you already know how to do it!
```python title="resources/user.py"
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from passlib.hash import pbkdf2_sha256
from db import db
from models import UserModel
from schemas import UserSchema
blp = Blueprint("Users", "users", description="Operations on users")
```
## Creating the `UserRegister` resource
Now let's create the `MethodView` class, and register a route to it using the blueprint:
```python title="resources/user.py"
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from passlib.hash import pbkdf2_sha256
from db import db
from models import UserModel
from schemas import UserSchema
blp = Blueprint("Users", "users", description="Operations on users")
# highlight-start
@blp.route("/register")
class UserRegister(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
if UserModel.query.filter(UserModel.username == user_data["username"]).first():
abort(409, message="A user with that username already exists.")
user = UserModel(
username=user_data["username"],
password=pbkdf2_sha256.hash(user_data["password"]),
)
db.session.add(user)
db.session.commit()
return {"message": "User created successfully."}, 201
# highlight-end
```
## Creating a testing-only `User` resource
Let's also create a `User` resource that we will only use during testing. It allows us to retrieve information about a single user, or delete a user. This will be handy so that using Insomnia or Postman we can clear the registered users and we don't have to change our request arguments each time!
```python title="resources/user.py"
@blp.route("/user/")
class User(MethodView):
"""
This resource can be useful when testing our Flask app.
We may not want to expose it to public users, but for the
sake of demonstration in this course, it can be useful
when we are manipulating data regarding the users.
"""
@blp.response(200, UserSchema)
def get(self, user_id):
user = UserModel.query.get_or_404(user_id)
return user
def delete(self, user_id):
user = UserModel.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return {"message": "User deleted."}, 200
```
## Register the user blueprint in `app.py`
Finally, let's go to `app.py` and register the blueprint!
```diff title="app.py"
+from resources.user import blp as UserBlueprint
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
...
+api.register_blueprint(UserBlueprint)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
```
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.user import blp as UserBlueprint
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(UserBlueprint)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/conftest.py
================================================
import pytest
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/__init__.py
================================================
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/models/user.py
================================================
from db import db
class UserModel(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
)
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client):
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client):
response = client.get(
"/item/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/__tests__/test_user.py
================================================
import pytest
@pytest.fixture()
def created_user_details(client):
username = "test_user"
password = "test_password"
client.post(
"/register",
json={"username": username, "password": password},
)
return username, password
def test_register_user(client):
username = "test_user"
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 201
assert response.json == {"message": "User created successfully."}
def test_register_user_already_exists(client):
username = "test_user"
client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 409
assert response.json["message"] == "A user with that username already exists."
def test_register_user_missing_data(client):
response = client.post(
"/register",
json={},
)
assert response.status_code == 422
assert "password" in response.json["errors"]["json"]
assert "username" in response.json["errors"]["json"]
def test_get_user_details(client, created_user_details):
response = client.get(
"/user/1", # assume user id is 1
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"username": created_user_details[0],
}
def test_get_user_details_missing(client):
response = client.get(
"/user/23",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/resources/user.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from passlib.hash import pbkdf2_sha256
from db import db
from models import UserModel
from schemas import UserSchema
blp = Blueprint("Users", "users", description="Operations on users")
@blp.route("/register")
class UserRegister(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
if UserModel.query.filter(UserModel.username == user_data["username"]).first():
abort(409, message="A user with that username already exists.")
user = UserModel(
username=user_data["username"],
password=pbkdf2_sha256.hash(user_data["password"]),
)
db.session.add(user)
db.session.commit()
return {"message": "User created successfully."}, 201
@blp.route("/user/")
class User(MethodView):
"""
This resource can be useful when testing our Flask app.
We may not want to expose it to public users, but for the
sake of demonstration in this course, it can be useful
when we are manipulating data regarding the users.
"""
@blp.response(200, UserSchema)
def get(self, user_id):
user = UserModel.query.get_or_404(user_id)
return user
def delete(self, user_id):
user = UserModel.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return {"message": "User deleted."}, 200
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
class UserSchema(Schema):
id = fields.Int(dump_only=True)
username = fields.Str(required=True)
password = fields.Str(required=True, load_only=True)
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/conftest.py
================================================
import pytest
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/__init__.py
================================================
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/models/user.py
================================================
from db import db
class UserModel(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
)
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client):
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client):
response = client.get(
"/item/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/06_registering_users_rest_api/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
class UserSchema(Schema):
id = fields.Int(dump_only=True)
username = fields.Str(required=True)
password = fields.Str(required=True, load_only=True)
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/README.md
================================================
---
title: How to add a login endpoint to the REST API
description: Learn how to add a login endpoint that returns a JWT to a REST API using Flask-Smorest and Flask-JWT-Extended.
ctslug: how-to-add-a-login-endpoint
---
# How to add a login endpoint to the REST API
Now that we've done registration, we can do log in! It's very similar.
Let's import `flask_jwt_extended.create_access_token` so that when we receive a valid username and password from the client, we can create a JWT and send it back:
```diff title="resources/user.py"
from flask.views import MethodView
from flask_smorest import Blueprint, abort
+from flask_jwt_extended import create_access_token
from passlib.hash import pbkdf2_sha256
```
Then let's create our `UserLogin` resource.
```python title="resources/user.py"
@blp.route("/login")
class UserLogin(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
user = UserModel.query.filter(
UserModel.username == user_data["username"]
).first()
if user and pbkdf2_sha256.verify(user_data["password"], user.password):
access_token = create_access_token(identity=str(user.id))
return {"access_token": access_token}, 200
abort(401, message="Invalid credentials.")
```
Here you can see the when we call `create_access_token(identity=str(user.id))` we pass in the user's `id`. This is what gets stored (among other things) inside the JWT, so when the client sends the JWT back on every request, we can tell who the JWT belongs to.
**Update Nov 2024**: Before now, we used `identity=user.id`, but now we have to convert it to a string first.
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/app.py
================================================
from flask import Flask
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.user import blp as UserBlueprint
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(UserBlueprint)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/conftest.py
================================================
import pytest
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/__init__.py
================================================
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/models/user.py
================================================
from db import db
class UserModel(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
)
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client):
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client):
response = client.get(
"/item/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/__tests__/test_user.py
================================================
import pytest
@pytest.fixture()
def created_user_details(client):
username = "test_user"
password = "test_password"
client.post(
"/register",
json={"username": username, "password": password},
)
return username, password
@pytest.fixture()
def created_user_jwt(client, created_user_details):
username, password = created_user_details
response = client.post(
"/login",
json={"username": username, "password": password},
)
return response.json["access_token"]
def test_register_user(client):
username = "test_user"
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 201
assert response.json == {"message": "User created successfully."}
def test_register_user_already_exists(client):
username = "test_user"
client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 409
assert response.json["message"] == "A user with that username already exists."
def test_register_user_missing_data(client):
response = client.post(
"/register",
json={},
)
assert response.status_code == 422
assert "password" in response.json["errors"]["json"]
assert "username" in response.json["errors"]["json"]
def test_login_user(client, created_user_details):
username, password = created_user_details
response = client.post(
"/login",
json={"username": username, "password": password},
)
assert response.status_code == 200
assert response.json["access_token"]
def test_login_user_bad_password(client, created_user_details):
username, _ = created_user_details
response = client.post(
"/login",
json={"username": username, "password": "bad_password"},
)
assert response.status_code == 401
assert response.json["message"] == "Invalid credentials."
def test_login_user_bad_username(client, created_user_details):
_, password = created_user_details
response = client.post(
"/login",
json={"username": "bad_username", "password": password},
)
assert response.status_code == 401
assert response.json["message"] == "Invalid credentials."
def test_get_user_details(client, created_user_details):
response = client.get(
"/user/1", # assume user id is 1
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"username": created_user_details[0],
}
def test_get_user_details_missing(client):
response = client.get(
"/user/23",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/resources/user.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from flask_jwt_extended import create_access_token
from passlib.hash import pbkdf2_sha256
from db import db
from models import UserModel
from schemas import UserSchema
blp = Blueprint("Users", "users", description="Operations on users")
@blp.route("/register")
class UserRegister(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
if UserModel.query.filter(UserModel.username == user_data["username"]).first():
abort(409, message="A user with that username already exists.")
user = UserModel(
username=user_data["username"],
password=pbkdf2_sha256.hash(user_data["password"]),
)
db.session.add(user)
db.session.commit()
return {"message": "User created successfully."}, 201
@blp.route("/login")
class UserLogin(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
user = UserModel.query.filter(
UserModel.username == user_data["username"]
).first()
if user and pbkdf2_sha256.verify(user_data["password"], user.password):
access_token = create_access_token(identity=str(user.id))
return {"access_token": access_token}, 200
abort(401, message="Invalid credentials.")
@blp.route("/user/")
class User(MethodView):
"""
This resource can be useful when testing our Flask app.
We may not want to expose it to public users, but for the
sake of demonstration in this course, it can be useful
when we are manipulating data regarding the users.
"""
@blp.response(200, UserSchema)
def get(self, user_id):
user = UserModel.query.get_or_404(user_id)
return user
def delete(self, user_id):
user = UserModel.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return {"message": "User deleted."}, 200
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
class UserSchema(Schema):
id = fields.Int(dump_only=True)
username = fields.Str(required=True)
password = fields.Str(required=True, load_only=True)
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.user import blp as UserBlueprint
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(UserBlueprint)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/conftest.py
================================================
import pytest
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/__init__.py
================================================
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/models/user.py
================================================
from db import db
class UserModel(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
)
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client):
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client):
response = client.get(
"/item/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/__tests__/test_user.py
================================================
import pytest
@pytest.fixture()
def created_user_details(client):
username = "test_user"
password = "test_password"
client.post(
"/register",
json={"username": username, "password": password},
)
return username, password
def test_register_user(client):
username = "test_user"
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 201
assert response.json == {"message": "User created successfully."}
def test_register_user_already_exists(client):
username = "test_user"
client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 409
assert response.json["message"] == "A user with that username already exists."
def test_register_user_missing_data(client):
response = client.post(
"/register",
json={},
)
assert response.status_code == 422
assert "password" in response.json["errors"]["json"]
assert "username" in response.json["errors"]["json"]
def test_get_user_details(client, created_user_details):
response = client.get(
"/user/1", # assume user id is 1
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"username": created_user_details[0],
}
def test_get_user_details_missing(client):
response = client.get(
"/user/23",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/resources/user.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from passlib.hash import pbkdf2_sha256
from db import db
from models import UserModel
from schemas import UserSchema
blp = Blueprint("Users", "users", description="Operations on users")
@blp.route("/register")
class UserRegister(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
if UserModel.query.filter(UserModel.username == user_data["username"]).first():
abort(409, message="A user with that username already exists.")
user = UserModel(
username=user_data["username"],
password=pbkdf2_sha256.hash(user_data["password"]),
)
db.session.add(user)
db.session.commit()
return {"message": "User created successfully."}, 201
@blp.route("/user/")
class User(MethodView):
"""
This resource can be useful when testing our Flask app.
We may not want to expose it to public users, but for the
sake of demonstration in this course, it can be useful
when we are manipulating data regarding the users.
"""
@blp.response(200, UserSchema)
def get(self, user_id):
user = UserModel.query.get_or_404(user_id)
return user
def delete(self, user_id):
user = UserModel.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return {"message": "User deleted."}, 200
================================================
FILE: docs/docs/08_flask_jwt_extended/07_login_users_rest_api/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
class UserSchema(Schema):
id = fields.Int(dump_only=True)
username = fields.Str(required=True)
password = fields.Str(required=True, load_only=True)
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/README.md
================================================
---
title: Protect endpoints by requiring a JWT
description: Use jwt_required from Flask-JWT-Extended to prevent unauthorised users from making requests to certain endpoints in a REST API.
ctslug: protect-endpoints-by-requiring-a-jwt
---
# Protect endpoints by requiring a JWT
Now that our users can sign up and log in, that means we can start _requiring login_ for certain endpoints.
All this means in practice is that the client making the request must send a valid JWT.
Remember, we can tell if a JWT is valid because it is _signed by our app_. If the user changes the JWT at all, the signature will be invalid, and we'll know it has been tampered with. Flask-JWT-Extended takes care of all that for us.
## Protecting routes in the `Item` resource
```python title="resources/item.py"
from flask.views import MethodView
from flask_smorest import Blueprint, abort
# highlight-start
from flask_jwt_extended import jwt_required
# highlight-end
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
# highlight-start
@jwt_required()
# highlight-end
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
# highlight-start
@jwt_required()
# highlight-end
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get_or_404(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(**item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
# highlight-start
@jwt_required()
# highlight-end
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
# highlight-start
@jwt_required()
# highlight-end
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
```
## Error handling with Flask-JWT-Extended
There are many things that could go wrong with JWTs:
- The JWT may be expired (they don't last forever!)
- The JWT may be invalid, such as if the client makes changes to it
- A JWT may be required, but none was provided
- There's more (we'll look at them in coming lectures!)
Let's go to `app.py` and add some configuration to tell Flask-JWT-Extended what to do in each of these cases.
At the top, let's import `jsonify`:
```python title="app.py"
from flask import Flask, jsonify
```
Then, after we define the `jwt = JWTManager(app)` variable, we can write some functions, each of which can run in different problem scenarios.
```python title="app.py"
...
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return (
jsonify({"message": "The token has expired.", "error": "token_expired"}),
401,
)
@jwt.invalid_token_loader
def invalid_token_callback(error):
return (
jsonify(
{"message": "Signature verification failed.", "error": "invalid_token"}
),
401,
)
@jwt.unauthorized_loader
def missing_token_callback(error):
return (
jsonify(
{
"description": "Request does not contain an access token.",
"error": "authorization_required",
}
),
401,
)
...
```
:::tip
Note that some Flask-JWT-Extended error functions take two arguments: `jwt_header` and `jwt_payload`. Others take a single argument, `error`.
The ones that don't take JWT information are those that would be called when a JWT is not present (above, when the JWT is invalid or required but not received).
:::
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/app.py
================================================
from flask import Flask, jsonify
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.user import blp as UserBlueprint
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return (
jsonify({"message": "The token has expired.", "error": "token_expired"}),
401,
)
@jwt.invalid_token_loader
def invalid_token_callback(error):
return (
jsonify(
{"message": "Signature verification failed.", "error": "invalid_token"}
),
401,
)
@jwt.unauthorized_loader
def missing_token_callback(error):
return (
jsonify(
{
"description": "Request does not contain an access token.",
"error": "authorization_required",
}
),
401,
)
@jwt.revoked_token_loader
def revoked_token_callback(jwt_header, jwt_payload):
return (
jsonify(
{"description": "The token has been revoked.", "error": "token_revoked"}
),
401,
)
# JWT configuration ends
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(UserBlueprint)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/conftest.py
================================================
import pytest
from flask_jwt_extended import create_access_token
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
@pytest.fixture()
def jwt(app):
with app.app_context():
access_token = create_access_token(identity=1)
return access_token
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/__init__.py
================================================
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/models/user.py
================================================
from db import db
class UserModel(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, jwt, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
headers={"Authorization": f"Bearer {jwt}"},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, jwt, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client, jwt):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client, jwt):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, jwt, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, jwt, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client, jwt):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.get(
"/item",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client, jwt):
response = client.get(
"/item",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, jwt, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, jwt, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client, jwt):
response = client.get(
"/item/1",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, jwt, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id, jwt):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client, jwt):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/__tests__/test_user.py
================================================
import pytest
@pytest.fixture()
def created_user_details(client):
username = "test_user"
password = "test_password"
client.post(
"/register",
json={"username": username, "password": password},
)
return username, password
@pytest.fixture()
def created_user_jwt(client, created_user_details):
username, password = created_user_details
response = client.post(
"/login",
json={"username": username, "password": password},
)
return response.json["access_token"]
def test_register_user(client):
username = "test_user"
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 201
assert response.json == {"message": "User created successfully."}
def test_register_user_already_exists(client):
username = "test_user"
client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 409
assert response.json["message"] == "A user with that username already exists."
def test_register_user_missing_data(client):
response = client.post(
"/register",
json={},
)
assert response.status_code == 422
assert "password" in response.json["errors"]["json"]
assert "username" in response.json["errors"]["json"]
def test_login_user(client, created_user_details):
username, password = created_user_details
response = client.post(
"/login",
json={"username": username, "password": password},
)
assert response.status_code == 200
assert response.json["access_token"]
def test_login_user_bad_password(client, created_user_details):
username, _ = created_user_details
response = client.post(
"/login",
json={"username": username, "password": "bad_password"},
)
assert response.status_code == 401
assert response.json["message"] == "Invalid credentials."
def test_login_user_bad_username(client, created_user_details):
_, password = created_user_details
response = client.post(
"/login",
json={"username": "bad_username", "password": password},
)
assert response.status_code == 401
assert response.json["message"] == "Invalid credentials."
def test_get_user_details(client, created_user_details):
response = client.get(
"/user/1", # assume user id is 1
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"username": created_user_details[0],
}
def test_get_user_details_missing(client):
response = client.get(
"/user/23",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from flask_jwt_extended import jwt_required
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@jwt_required()
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
@jwt_required()
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@jwt_required()
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@jwt_required()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/resources/user.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from flask_jwt_extended import create_access_token
from passlib.hash import pbkdf2_sha256
from db import db
from models import UserModel
from schemas import UserSchema
blp = Blueprint("Users", "users", description="Operations on users")
@blp.route("/register")
class UserRegister(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
if UserModel.query.filter(UserModel.username == user_data["username"]).first():
abort(409, message="A user with that username already exists.")
user = UserModel(
username=user_data["username"],
password=pbkdf2_sha256.hash(user_data["password"]),
)
db.session.add(user)
db.session.commit()
return {"message": "User created successfully."}, 201
@blp.route("/login")
class UserLogin(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
user = UserModel.query.filter(
UserModel.username == user_data["username"]
).first()
if user and pbkdf2_sha256.verify(user_data["password"], user.password):
access_token = create_access_token(identity=str(user.id))
return {"access_token": access_token}, 200
abort(401, message="Invalid credentials.")
@blp.route("/user/")
class User(MethodView):
"""
This resource can be useful when testing our Flask app.
We may not want to expose it to public users, but for the
sake of demonstration in this course, it can be useful
when we are manipulating data regarding the users.
"""
@blp.response(200, UserSchema)
def get(self, user_id):
user = UserModel.query.get_or_404(user_id)
return user
def delete(self, user_id):
user = UserModel.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return {"message": "User deleted."}, 200
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
class UserSchema(Schema):
id = fields.Int(dump_only=True)
username = fields.Str(required=True)
password = fields.Str(required=True, load_only=True)
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/app.py
================================================
from flask import Flask
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.user import blp as UserBlueprint
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(UserBlueprint)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/conftest.py
================================================
import pytest
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/__init__.py
================================================
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship(
"ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/models/user.py
================================================
from db import db
class UserModel(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
)
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client):
response = client.get(
"/item",
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client):
response = client.get(
"/item/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/__tests__/test_user.py
================================================
import pytest
@pytest.fixture()
def created_user_details(client):
username = "test_user"
password = "test_password"
client.post(
"/register",
json={"username": username, "password": password},
)
return username, password
@pytest.fixture()
def created_user_jwt(client, created_user_details):
username, password = created_user_details
response = client.post(
"/login",
json={"username": username, "password": password},
)
return response.json["access_token"]
def test_register_user(client):
username = "test_user"
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 201
assert response.json == {"message": "User created successfully."}
def test_register_user_already_exists(client):
username = "test_user"
client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 409
assert response.json["message"] == "A user with that username already exists."
def test_register_user_missing_data(client):
response = client.post(
"/register",
json={},
)
assert response.status_code == 422
assert "password" in response.json["errors"]["json"]
assert "username" in response.json["errors"]["json"]
def test_login_user(client, created_user_details):
username, password = created_user_details
response = client.post(
"/login",
json={"username": username, "password": password},
)
assert response.status_code == 200
assert response.json["access_token"]
def test_login_user_bad_password(client, created_user_details):
username, _ = created_user_details
response = client.post(
"/login",
json={"username": username, "password": "bad_password"},
)
assert response.status_code == 401
assert response.json["message"] == "Invalid credentials."
def test_login_user_bad_username(client, created_user_details):
_, password = created_user_details
response = client.post(
"/login",
json={"username": "bad_username", "password": password},
)
assert response.status_code == 401
assert response.json["message"] == "Invalid credentials."
def test_get_user_details(client, created_user_details):
response = client.get(
"/user/1", # assume user id is 1
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"username": created_user_details[0],
}
def test_get_user_details_missing(client):
response = client.get(
"/user/23",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/resources/user.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from flask_jwt_extended import create_access_token
from passlib.hash import pbkdf2_sha256
from db import db
from models import UserModel
from schemas import UserSchema
blp = Blueprint("Users", "users", description="Operations on users")
@blp.route("/register")
class UserRegister(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
if UserModel.query.filter(UserModel.username == user_data["username"]).first():
abort(409, message="A user with that username already exists.")
user = UserModel(
username=user_data["username"],
password=pbkdf2_sha256.hash(user_data["password"]),
)
db.session.add(user)
db.session.commit()
return {"message": "User created successfully."}, 201
@blp.route("/login")
class UserLogin(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
user = UserModel.query.filter(
UserModel.username == user_data["username"]
).first()
if user and pbkdf2_sha256.verify(user_data["password"], user.password):
access_token = create_access_token(identity=str(user.id))
return {"access_token": access_token}, 200
abort(401, message="Invalid credentials.")
@blp.route("/user/")
class User(MethodView):
"""
This resource can be useful when testing our Flask app.
We may not want to expose it to public users, but for the
sake of demonstration in this course, it can be useful
when we are manipulating data regarding the users.
"""
@blp.response(200, UserSchema)
def get(self, user_id):
user = UserModel.query.get_or_404(user_id)
return user
def delete(self, user_id):
user = UserModel.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return {"message": "User deleted."}, 200
================================================
FILE: docs/docs/08_flask_jwt_extended/08_protect_resources_with_jwt_required/start/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
class UserSchema(Schema):
id = fields.Int(dump_only=True)
username = fields.Str(required=True)
password = fields.Str(required=True, load_only=True)
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/README.md
================================================
---
title: JWT claims and authorization
description: Learn how to add claims (extra info) to a JWT and use it for authorization in endpoints of a REST API.
ctslug: jwt-claims-and-authorization
---
# JWT Claims and Authorization
JWT claims are extra data we can add to the JWT. For example, we could store in the JWT whether the user whose ID is stored in the JWT is an "administrator" or not.
By doing this, we only have to check the user's permissions once, when we create the JWT, and not every time the user makes a request.
To add a custom claim to a JWT we define a function similar to the error handling functions we wrote in the last lecture:
```python title="app.py"
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
# highlight-start
@jwt.additional_claims_loader
def add_claims_to_jwt(identity):
if identity == 1:
return {"is_admin": True}
return {"is_admin": False}
# highlight-end
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return (
jsonify({"message": "The token has expired.", "error": "token_expired"}),
401,
)
```
:::caution Read from a database or config file
Here we're assuming that the user with and ID of `1` will be the administrator. Normally you'd read this from either a config file or the database.
:::
## How to use JWT claims in an endpoint
Let's make a small change to the `Item` resource so that only admins can delete items.
To do so, we'll need to add an import for `get_jwt`:
```python title="resources/item.py"
from flask_jwt_extended import jwt_required, get_jwt
```
Then in the `delete` endpoint, we can use `get_jwt()` to check the data in the JWT (which behaves like a dictionary):
```python title="resources/item.py"
@jwt_required()
def delete(self, item_id):
# highlight-start
jwt = get_jwt()
if not jwt.get("is_admin"):
abort(401, message="Admin privilege required.")
# highlight-end
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
```
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/app.py
================================================
from flask import Flask, jsonify
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.user import blp as UserBlueprint
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
# @jwt.additional_claims_loader
# def add_claims_to_jwt(identity):
# # TODO: Read from a config file instead of hard-coding
# if identity == 1:
# return {"is_admin": True}
# return {"is_admin": False}
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return (
jsonify({"message": "The token has expired.", "error": "token_expired"}),
401,
)
@jwt.invalid_token_loader
def invalid_token_callback(error):
return (
jsonify(
{"message": "Signature verification failed.", "error": "invalid_token"}
),
401,
)
@jwt.unauthorized_loader
def missing_token_callback(error):
return (
jsonify(
{
"description": "Request does not contain an access token.",
"error": "authorization_required",
}
),
401,
)
@jwt.revoked_token_loader
def revoked_token_callback(jwt_header, jwt_payload):
return (
jsonify(
{"description": "The token has been revoked.", "error": "token_revoked"}
),
401,
)
# JWT configuration ends
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(UserBlueprint)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/conftest.py
================================================
import pytest
from flask_jwt_extended import create_access_token
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
@pytest.fixture()
def jwt(app):
with app.app_context():
access_token = create_access_token(identity=1)
return access_token
@pytest.fixture()
def admin_jwt(app):
with app.app_context():
access_token = create_access_token(
identity=1, additional_claims={"is_admin": True}
)
return access_token
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/__init__.py
================================================
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/models/user.py
================================================
from db import db
class UserModel(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, jwt, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
headers={"Authorization": f"Bearer {jwt}"},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, jwt, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client, jwt):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client, jwt):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, admin_jwt, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
headers={"Authorization": f"Bearer {admin_jwt}"},
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_delete_item_without_admin(client, jwt, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 401
assert response.json["message"] == "Admin privilege required."
def test_update_item(client, jwt, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client, jwt):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.get(
"/item",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client, jwt):
response = client.get(
"/item",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, jwt, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, jwt, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client, jwt):
response = client.get(
"/item/1",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, jwt, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id, jwt):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client, jwt):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/__tests__/test_user.py
================================================
import pytest
@pytest.fixture()
def created_user_details(client):
username = "test_user"
password = "test_password"
client.post(
"/register",
json={"username": username, "password": password},
)
return username, password
@pytest.fixture()
def created_user_jwt(client, created_user_details):
username, password = created_user_details
response = client.post(
"/login",
json={"username": username, "password": password},
)
return response.json["access_token"]
def test_register_user(client):
username = "test_user"
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 201
assert response.json == {"message": "User created successfully."}
def test_register_user_already_exists(client):
username = "test_user"
client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 409
assert response.json["message"] == "A user with that username already exists."
def test_register_user_missing_data(client):
response = client.post(
"/register",
json={},
)
assert response.status_code == 422
assert "password" in response.json["errors"]["json"]
assert "username" in response.json["errors"]["json"]
def test_login_user(client, created_user_details):
username, password = created_user_details
response = client.post(
"/login",
json={"username": username, "password": password},
)
assert response.status_code == 200
assert response.json["access_token"]
def test_login_user_bad_password(client, created_user_details):
username, _ = created_user_details
response = client.post(
"/login",
json={"username": username, "password": "bad_password"},
)
assert response.status_code == 401
assert response.json["message"] == "Invalid credentials."
def test_login_user_bad_username(client, created_user_details):
_, password = created_user_details
response = client.post(
"/login",
json={"username": "bad_username", "password": password},
)
assert response.status_code == 401
assert response.json["message"] == "Invalid credentials."
def test_get_user_details(client, created_user_details):
response = client.get(
"/user/1", # assume user id is 1
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"username": created_user_details[0],
}
def test_get_user_details_missing(client):
response = client.get(
"/user/23",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from flask_jwt_extended import jwt_required, get_jwt
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@jwt_required()
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
@jwt_required()
def delete(self, item_id):
jwt = get_jwt()
if not jwt.get("is_admin"):
abort(401, message="Admin privilege required.")
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@jwt_required()
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@jwt_required()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/")
class Tag(MethodView):
@blp.response(200, TagSchema)
def get(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
return tag
@blp.response(
202,
description="Deletes a tag if no item is tagged with it.",
example={"message": "Tag deleted."},
)
@blp.alt_response(404, description="Tag not found.")
@blp.alt_response(
400,
description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted.",
)
def delete(self, tag_id):
tag = TagModel.query.get_or_404(tag_id)
if not tag.items:
db.session.delete(tag)
db.session.commit()
return {"message": "Tag deleted."}
abort(
400,
message="Could not delete tag. Make sure tag is not associated with any items, then try again.", # noqa: E501
)
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/resources/user.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from flask_jwt_extended import create_access_token
from passlib.hash import pbkdf2_sha256
from db import db
from models import UserModel
from schemas import UserSchema
blp = Blueprint("Users", "users", description="Operations on users")
@blp.route("/register")
class UserRegister(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
if UserModel.query.filter(UserModel.username == user_data["username"]).first():
abort(409, message="A user with that username already exists.")
user = UserModel(
username=user_data["username"],
password=pbkdf2_sha256.hash(user_data["password"]),
)
db.session.add(user)
db.session.commit()
return {"message": "User created successfully."}, 201
@blp.route("/login")
class UserLogin(MethodView):
@blp.arguments(UserSchema)
def post(self, user_data):
user = UserModel.query.filter(
UserModel.username == user_data["username"]
).first()
if user and pbkdf2_sha256.verify(user_data["password"], user.password):
access_token = create_access_token(identity=str(user.id))
return {"access_token": access_token}, 200
abort(401, message="Invalid credentials.")
@blp.route("/user/")
class User(MethodView):
"""
This resource can be useful when testing our Flask app.
We may not want to expose it to public users, but for the
sake of demonstration in this course, it can be useful
when we are manipulating data regarding the users.
"""
@blp.response(200, UserSchema)
def get(self, user_id):
user = UserModel.query.get_or_404(user_id)
return user
def delete(self, user_id):
user = UserModel.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return {"message": "User deleted."}, 200
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/end/schemas.py
================================================
from marshmallow import Schema, fields
class PlainItemSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
price = fields.Float(required=True)
class PlainStoreSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class PlainTagSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str()
class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class ItemUpdateSchema(Schema):
name = fields.Str()
price = fields.Float()
class StoreSchema(PlainStoreSchema):
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True)
class TagSchema(PlainTagSchema):
store_id = fields.Int(load_only=True)
items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)
class TagAndItemSchema(Schema):
message = fields.Str()
item = fields.Nested(ItemSchema)
tag = fields.Nested(TagSchema)
class UserSchema(Schema):
id = fields.Int(dump_only=True)
username = fields.Str(required=True)
password = fields.Str(required=True, load_only=True)
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/.dockerignore
================================================
.venv
*.pyc
__pycache__
data.db
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/.flake8
================================================
[flake8]
per-file-ignores = __init__.py:F401
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/.flaskenv
================================================
FLASK_APP=app
FLASK_DEBUG=True
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/Dockerfile
================================================
FROM python:3.10
EXPOSE 5000
WORKDIR /app
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY . .
CMD ["flask", "run", "--host", "0.0.0.0"]
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/app.py
================================================
from flask import Flask, jsonify
from flask_smorest import Api
from flask_jwt_extended import JWTManager
from db import db
from resources.user import blp as UserBlueprint
from resources.item import blp as ItemBlueprint
from resources.store import blp as StoreBlueprint
from resources.tag import blp as TagBlueprint
def create_app(db_url=None):
app = Flask(__name__)
app.config["API_TITLE"] = "Stores REST API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.3"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config[
"OPENAPI_SWAGGER_UI_URL"
] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = db_url or "sqlite:///data.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["PROPAGATE_EXCEPTIONS"] = True
db.init_app(app)
api = Api(app)
app.config["JWT_SECRET_KEY"] = "jose"
jwt = JWTManager(app)
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return (
jsonify({"message": "The token has expired.", "error": "token_expired"}),
401,
)
@jwt.invalid_token_loader
def invalid_token_callback(error):
return (
jsonify(
{"message": "Signature verification failed.", "error": "invalid_token"}
),
401,
)
@jwt.unauthorized_loader
def missing_token_callback(error):
return (
jsonify(
{
"description": "Request does not contain an access token.",
"error": "authorization_required",
}
),
401,
)
@jwt.revoked_token_loader
def revoked_token_callback(jwt_header, jwt_payload):
return (
jsonify(
{"description": "The token has been revoked.", "error": "token_revoked"}
),
401,
)
# JWT configuration ends
with app.app_context():
import models # noqa: F401
db.create_all()
api.register_blueprint(UserBlueprint)
api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
api.register_blueprint(TagBlueprint)
return app
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/conftest.py
================================================
import pytest
from flask_jwt_extended import create_access_token
from app import create_app
@pytest.fixture()
def app():
app = create_app("sqlite://")
app.config.update(
{
"TESTING": True,
}
)
yield app
@pytest.fixture()
def client(app):
return app.test_client()
@pytest.fixture()
def jwt(app):
with app.app_context():
access_token = create_access_token(identity=1)
return access_token
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/db.py
================================================
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/__init__.py
================================================
from models.user import UserModel
from models.item import ItemModel
from models.tag import TagModel
from models.store import StoreModel
from models.item_tags import ItemsTags
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/item.py
================================================
from db import db
class ItemModel(db.Model):
__tablename__ = "items"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
price = db.Column(db.Float(precision=2), unique=False, nullable=False)
store_id = db.Column(
db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False
)
store = db.relationship("StoreModel", back_populates="items")
tags = db.relationship("TagModel", back_populates="items", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/item_tags.py
================================================
from db import db
class ItemsTags(db.Model):
__tablename__ = "items_tags"
id = db.Column(db.Integer, primary_key=True)
item_id = db.Column(db.Integer, db.ForeignKey("items.id"))
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/store.py
================================================
from db import db
class StoreModel(db.Model):
__tablename__ = "stores"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)
tags = db.relationship("TagModel", back_populates="store", lazy="dynamic")
items = db.relationship("ItemModel", back_populates="store", lazy="dynamic")
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/tag.py
================================================
from db import db
class TagModel(db.Model):
__tablename__ = "tags"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=False, nullable=False)
store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False)
store = db.relationship("StoreModel", back_populates="tags")
items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags")
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/models/user.py
================================================
from db import db
class UserModel(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(80), nullable=False)
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/requirements-dev.txt
================================================
pytest
black
flake8
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/requirements.txt
================================================
Flask-JWT-Extended
Flask-Smorest
Flask-SQLAlchemy
passlib
marshmallow
python-dotenv
gunicorn
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__init__.py
================================================
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__tests__/conftest.py
================================================
import pytest
@pytest.fixture()
def created_store_id(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
return response.json["id"]
@pytest.fixture()
def created_item_id(client, jwt, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
headers={"Authorization": f"Bearer {jwt}"},
)
return response.json["id"]
@pytest.fixture()
def created_tag_id(client, created_store_id):
response = client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
return response.json["id"]
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__tests__/test_item.py
================================================
def test_create_item_in_store(client, jwt, created_store_id):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_create_item_with_store_id_not_found(client, jwt):
# Note that this will fail if foreign key constraints are enabled.
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] is None
def test_create_item_with_unknown_data(client, jwt):
response = client.post(
"/item",
json={
"name": "Test Item",
"price": 10.5,
"store_id": 1,
"unknown_field": "unknown",
},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 422
assert response.json["errors"]["json"]["unknown_field"] == ["Unknown field."]
def test_delete_item(client, jwt, created_item_id):
response = client.delete(
f"/item/{created_item_id}",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["message"] == "Item deleted."
def test_update_item(client, jwt, created_item_id):
response = client.put(
f"/item/{created_item_id}",
json={"name": "Test Item (updated)", "price": 12.5},
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item (updated)"
assert response.json["price"] == 12.5
def test_get_all_items(client, jwt):
response = client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.post(
"/item",
json={"name": "Test Item 2", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.get(
"/item",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert len(response.json) == 2
assert response.json[0]["name"] == "Test Item"
assert response.json[0]["price"] == 10.5
assert response.json[1]["name"] == "Test Item 2"
def test_get_all_items_empty(client, jwt):
response = client.get(
"/item",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert len(response.json) == 0
def test_get_item_details(client, jwt, created_item_id, created_store_id):
response = client.get(
f"/item/{created_item_id}",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["store"] == {"id": created_store_id, "name": "Test Store"}
def test_get_item_details_with_tag(client, jwt, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/item/{created_item_id}",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 200
assert response.json["name"] == "Test Item"
assert response.json["price"] == 10.5
assert response.json["tags"] == [{"id": created_tag_id, "name": "Test Tag"}]
def test_get_item_detail_not_found(client, jwt):
response = client.get(
"/item/1",
headers={"Authorization": f"Bearer {jwt}"},
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__tests__/test_store.py
================================================
def test_get_store(client, created_store_id):
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Store",
"items": [],
"tags": [],
}
def test_get_store_not_found(client):
response = client.get(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_with_item(client, jwt, created_store_id):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": created_store_id},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_get_store_with_tag(client, created_store_id):
client.post(
f"/store/{created_store_id}/tag",
json={"name": "Test Tag"},
)
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["tags"] == [{"id": 1, "name": "Test Tag"}]
def test_create_store(client):
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 201
assert response.json["name"] == "Test Store"
def test_create_store_with_items(client, created_store_id, jwt):
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
# Get the store with id 1 and check the items contains the newly created item
response = client.get(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_delete_store(client, created_store_id):
response = client.delete(
f"/store/{created_store_id}",
)
assert response.status_code == 200
assert response.json == {"message": "Store deleted"}
def test_delete_store_doesnt_exist(client):
response = client.delete(
"/store/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_store_list_empty(client):
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == []
def test_get_store_list_single(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [{"id": 1, "name": "Test Store", "items": [], "tags": []}]
def test_get_store_list_multiple(client):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/store",
json={"name": "Test Store 2"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{"id": 1, "name": "Test Store", "items": [], "tags": []},
{"id": 2, "name": "Test Store 2", "items": [], "tags": []},
]
def test_get_store_list_with_items(client, jwt):
client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
"/item",
json={"name": "Test Item", "price": 10.5, "store_id": 1},
headers={"Authorization": f"Bearer {jwt}"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
],
"tags": [],
}
]
def test_get_store_list_with_tags(client):
resp = client.post(
"/store",
json={"name": "Test Store"},
)
client.post(
f"/store/{resp.json['id']}/tag",
json={"name": "Test Tag"},
)
response = client.get(
"/store",
)
assert response.status_code == 200
assert response.json == [
{
"id": 1,
"name": "Test Store",
"items": [],
"tags": [{"id": 1, "name": "Test Tag"}],
}
]
def test_create_store_duplicate_name(client):
client.post(
"/store",
json={"name": "Test Store"},
)
response = client.post(
"/store",
json={"name": "Test Store"},
)
assert response.status_code == 400
assert response.json["message"] == "A store with that name already exists."
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__tests__/test_tag.py
================================================
import pytest
import logging
LOGGER = logging.getLogger(__name__)
@pytest.fixture()
def created_tag_with_item_id(client, created_item_id, created_tag_id):
client.post(f"/item/{created_item_id}/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
return response.json["id"]
def test_get_tag(client, created_tag_id):
response = client.get(
f"/tag/{created_tag_id}",
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
def test_get_tag_not_found(client):
response = client.get(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_items_linked_with_tag(client, created_tag_with_item_id):
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == [
{
"id": 1,
"name": "Test Item",
"price": 10.5,
}
]
def test_unlink_tag_from_item(client, created_item_id, created_tag_with_item_id):
client.delete(f"/item/{created_item_id}/tag/{created_tag_with_item_id}")
response = client.get(
f"/tag/{created_tag_with_item_id}",
)
assert response.status_code == 200
assert response.json["items"] == []
def test_delete_tag_without_items(client, created_tag_id):
delete_response = client.delete(f"/tag/{created_tag_id}")
response = client.get(
f"/tag/{created_tag_id}",
)
assert delete_response.status_code == 202
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_delete_tag_still_has_items(client, created_tag_with_item_id):
response = client.delete(f"/tag/{created_tag_with_item_id}")
assert response.status_code == 400
assert (
response.json["message"]
== "Could not delete tag. Make sure tag is not associated with any items, then try again."
)
def test_delete_tag_not_found(client):
response = client.delete(
"/tag/1",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
def test_get_all_tags_in_store(client, created_store_id, created_tag_id):
response = client.get(
f"/store/{created_store_id}/tag",
)
assert response.status_code == 200
assert response.json == [
{
"id": created_tag_id,
"name": "Test Tag",
"items": [],
"store": {"id": 1, "name": "Test Store"},
}
]
def test_get_all_tags_in_store_not_found(client):
response = client.get(
"/store/1/tag",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/__tests__/test_user.py
================================================
import pytest
@pytest.fixture()
def created_user_details(client):
username = "test_user"
password = "test_password"
client.post(
"/register",
json={"username": username, "password": password},
)
return username, password
@pytest.fixture()
def created_user_jwt(client, created_user_details):
username, password = created_user_details
response = client.post(
"/login",
json={"username": username, "password": password},
)
return response.json["access_token"]
def test_register_user(client):
username = "test_user"
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 201
assert response.json == {"message": "User created successfully."}
def test_register_user_already_exists(client):
username = "test_user"
client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
response = client.post(
"/register",
json={"username": username, "password": "Test Password"},
)
assert response.status_code == 409
assert response.json["message"] == "A user with that username already exists."
def test_register_user_missing_data(client):
response = client.post(
"/register",
json={},
)
assert response.status_code == 422
assert "password" in response.json["errors"]["json"]
assert "username" in response.json["errors"]["json"]
def test_login_user(client, created_user_details):
username, password = created_user_details
response = client.post(
"/login",
json={"username": username, "password": password},
)
assert response.status_code == 200
assert response.json["access_token"]
def test_login_user_bad_password(client, created_user_details):
username, _ = created_user_details
response = client.post(
"/login",
json={"username": username, "password": "bad_password"},
)
assert response.status_code == 401
assert response.json["message"] == "Invalid credentials."
def test_login_user_bad_username(client, created_user_details):
_, password = created_user_details
response = client.post(
"/login",
json={"username": "bad_username", "password": password},
)
assert response.status_code == 401
assert response.json["message"] == "Invalid credentials."
def test_get_user_details(client, created_user_details):
response = client.get(
"/user/1", # assume user id is 1
)
assert response.status_code == 200
assert response.json == {
"id": 1,
"username": created_user_details[0],
}
def test_get_user_details_missing(client):
response = client.get(
"/user/23",
)
assert response.status_code == 404
assert response.json == {"code": 404, "status": "Not Found"}
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/item.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from flask_jwt_extended import jwt_required
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import ItemModel
from schemas import ItemSchema, ItemUpdateSchema
blp = Blueprint("Items", __name__, description="Operations on items")
@blp.route("/item/")
class Item(MethodView):
@jwt_required()
@blp.response(200, ItemSchema)
def get(self, item_id):
item = ItemModel.query.get_or_404(item_id)
return item
@jwt_required()
def delete(self, item_id):
item = ItemModel.query.get_or_404(item_id)
db.session.delete(item)
db.session.commit()
return {"message": "Item deleted."}
@blp.arguments(ItemUpdateSchema)
@blp.response(200, ItemSchema)
def put(self, item_data, item_id):
item = ItemModel.query.get(item_id)
if item:
item.price = item_data["price"]
item.name = item_data["name"]
else:
item = ItemModel(id=item_id, **item_data)
db.session.add(item)
db.session.commit()
return item
@blp.route("/item")
class ItemList(MethodView):
@jwt_required()
@blp.response(200, ItemSchema(many=True))
def get(self):
return ItemModel.query.all()
@jwt_required()
@blp.arguments(ItemSchema)
@blp.response(201, ItemSchema)
def post(self, item_data):
item = ItemModel(**item_data)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the item.")
return item
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/store.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
from db import db
from models import StoreModel
from schemas import StoreSchema
blp = Blueprint("Stores", __name__, description="Operations on stores")
@blp.route("/store/")
class Store(MethodView):
@blp.response(200, StoreSchema)
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store
def delete(self, store_id):
store = StoreModel.query.get_or_404(store_id)
db.session.delete(store)
db.session.commit()
return {"message": "Store deleted"}, 200
@blp.route("/store")
class StoreList(MethodView):
@blp.response(200, StoreSchema(many=True))
def get(self):
return StoreModel.query.all()
@blp.arguments(StoreSchema)
@blp.response(201, StoreSchema)
def post(self, store_data):
store = StoreModel(**store_data)
try:
db.session.add(store)
db.session.commit()
except IntegrityError:
abort(
400,
message="A store with that name already exists.",
)
except SQLAlchemyError:
abort(500, message="An error occurred creating the store.")
return store
================================================
FILE: docs/docs/08_flask_jwt_extended/09_jwt_claims_and_authorization/start/resources/tag.py
================================================
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import SQLAlchemyError
from db import db
from models import TagModel, StoreModel, ItemModel
from schemas import TagSchema, TagAndItemSchema
blp = Blueprint("Tags", "tags", description="Operations on tags")
@blp.route("/store//tag")
class TagsInStore(MethodView):
@blp.response(200, TagSchema(many=True))
def get(self, store_id):
store = StoreModel.query.get_or_404(store_id)
return store.tags.all() # lazy="dynamic" means 'tags' is a query
@blp.arguments(TagSchema)
@blp.response(201, TagSchema)
def post(self, tag_data, store_id):
if TagModel.query.filter(TagModel.store_id == store_id, TagModel.name == tag_data["name"]).first():
abort(400, message="A tag with that name already exists in that store.")
tag = TagModel(**tag_data, store_id=store_id)
try:
db.session.add(tag)
db.session.commit()
except SQLAlchemyError as e:
abort(
500,
message=str(e),
)
return tag
@blp.route("/item//tag/")
class LinkTagsToItem(MethodView):
@blp.response(201, TagSchema)
def post(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.append(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return tag
@blp.response(200, TagAndItemSchema)
def delete(self, item_id, tag_id):
item = ItemModel.query.get_or_404(item_id)
tag = TagModel.query.get_or_404(tag_id)
item.tags.remove(tag)
try:
db.session.add(item)
db.session.commit()
except SQLAlchemyError:
abort(500, message="An error occurred while inserting the tag.")
return {"message": "Item removed from tag", "item": item, "tag": tag}
@blp.route("/tag/