Skip to content

Commit

Permalink
Merge pull request #51 from tecladocode/jose/cou-103-write-sqlalchemy…
Browse files Browse the repository at this point in the history
…-many-to-many-seection
  • Loading branch information
jslvtr authored Jun 1, 2022
2 parents 0b98f3b + 22842eb commit 81c4c5a
Show file tree
Hide file tree
Showing 98 changed files with 2,562 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class PlainStoreSchema(Schema):

class ItemSchema(PlainItemSchema):
store_id = fields.Int(required=True, load_only=True)
store = fields.Nested(lambda: PlainStoreSchema(), dump_only=True)
store = fields.Nested(PlainStoreSchema(), dump_only=True)


class ItemUpdateSchema(Schema):
Expand Down
43 changes: 43 additions & 0 deletions docs/docs/07_sqlalchemy_many_to_many/01_section_changes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
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.
---

# Changes in this section

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:

![ER database model showing relationships](./assets/db_model.drawio.png)

## New API endpoints to be added

In this section we will add all the Tag endpoints:


| Method | Endpoint | Description |
| -------- | ----------------------- | ------------------------------------------------------- |
| `GET` | `/stores/{id}/tags` | Get a list of tags in a store. |
| `POST` | `/stores/{id}/tags` | Create a new tag. |
| `POST` | `/items/{id}/tags/{id}` | Link an item in a store with a tag from the same store. |
| `DELETE` | `/items/{id}/tags/{id}` | Unlink a tag from an item. |
| `GET` | `/tags/{id}` | Get information about a tag given its unique id. |
| `DELETE` | `/tags/{id}` | Delete a tag, which must have no associated items. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
195 changes: 195 additions & 0 deletions docs/docs/07_sqlalchemy_many_to_many/02_one_to_many_review/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
---
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.
---

- [x] Set metadata above
- [x] Start writing!
- [x] Create `start` folder
- [x] Create `end` folder
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")

# 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';

<div className="codeTabContainer">
<Tabs>
<TabItem value="tag" label="models/tag.py" default>

```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=True, nullable=False)
store_id = db.Column(db.String(), db.ForeignKey("stores.id"), nullable=False)

store = db.relationship("StoreModel", back_populates="tags")
```

</TabItem>
<TabItem value="store" label="models/store.py">

```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
```

</TabItem>
</Tabs>
</div>

## 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` | `/stores/{id}/tags` | Get a list of tags in a store. |
|`POST` | `/stores/{id}/tags` | Create a new tag. |
|`POST` | `/items/{id}/tags/{id}` | Link an item in a store with a tag from the same store. |
|`DELETE` | `/items/{id}/tags/{id}` | Unlink a tag from an item. |
|`GET` | `/tags/{id}` | Get information about a tag given its unique id. |
|`DELETE` | `/tags/{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("/stores/<string:store_id>/tags")
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).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("/tags/<string:tag_id>")
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)

@app.before_first_request
def create_tables():
db.create_all()

api.register_blueprint(ItemBlueprint)
api.register_blueprint(StoreBlueprint)
# highlight-start
api.register_blueprint(TagBlueprint)
# highlight-end

return app
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FLASK_APP=app
FLASK_ENV=development
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
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"]
Loading

1 comment on commit 81c4c5a

@vercel
Copy link

@vercel vercel bot commented on 81c4c5a Jun 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.