Skip to content

Commit 027a0d2

Browse files
authored
Merge pull request #57 from tecladocode/jose/improve-smorest-section
2 parents 73ffdbc + 5f6d70f commit 027a0d2

File tree

115 files changed

+1433
-505
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

115 files changed

+1433
-505
lines changed
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
---
2+
title: "Data model improvements"
3+
description: "Use dictionaries instead of lists for data storage, and store stores and items separately."
4+
---
5+
6+
# Data model improvements
7+
8+
- [x] Set metadata above
9+
- [x] Start writing!
10+
- [x] Create `start` folder
11+
- [x] Create `end` folder
12+
- [ ] Create per-file diff between `end` and `start` (use "Compare Folders")
13+
14+
## Starting code from section 4
15+
16+
This is the "First REST API" project from Section 4:
17+
18+
import Tabs from '@theme/Tabs';
19+
import TabItem from '@theme/TabItem';
20+
21+
<div className="codeTabContainer">
22+
<Tabs>
23+
<TabItem value="app" label="app.py" default>
24+
25+
```py title="app.py"
26+
from flask import Flask, request
27+
28+
app = Flask(__name__)
29+
30+
stores = [
31+
{
32+
"name": "My Store",
33+
"items": [
34+
{
35+
"name": "Chair",
36+
"price": 15.99
37+
}
38+
]
39+
}
40+
]
41+
42+
@app.get("/store") # http://127.0.0.1:5000/store
43+
def get_stores():
44+
return {"stores": stores}
45+
46+
47+
@app.post("/store")
48+
def create_store():
49+
request_data = request.get_json()
50+
new_store = {"name": request_data["name"], "items": []}
51+
stores.append(new_store)
52+
return new_store, 201
53+
54+
55+
@app.post("/store/<string:name>/item")
56+
def create_item(name):
57+
request_data = request.get_json()
58+
for store in stores:
59+
if store["name"] == name:
60+
new_item = {"name": request_data["name"], "price": request_data["price"]}
61+
store["items"].append(new_item)
62+
return new_item, 201
63+
return {"message": "Store not found"}, 404
64+
65+
66+
@app.get("/store/<string:name>")
67+
def get_store(name):
68+
for store in stores:
69+
if store["name"] == name:
70+
return store
71+
return {"message": "Store not found"}, 404
72+
73+
74+
@app.get("/store/<string:name>/item")
75+
def get_item_in_store(name):
76+
for store in stores:
77+
if store["name"] == name:
78+
return {"items": store["items"]}
79+
return {"message": "Store not found"}, 404
80+
```
81+
82+
</TabItem>
83+
<TabItem value="docker" label="Dockerfile">
84+
85+
```docker
86+
FROM python:3.10
87+
EXPOSE 5000
88+
WORKDIR /app
89+
RUN pip install flask
90+
COPY . .
91+
CMD ["flask", "run", "--host", "0.0.0.0"]
92+
```
93+
94+
</TabItem>
95+
</Tabs>
96+
</div>
97+
98+
99+
## New files
100+
101+
Let's start off by creating a `requirements.txt` file with all our dependencies:
102+
103+
```txt title="requirements.txt"
104+
flask
105+
flask-smorest
106+
python-dotenv
107+
```
108+
109+
We're adding `flask-smorest` to help us write REST APIs more easily, and generate documentation for us.
110+
111+
We're adding `python-dotenv` so it's easier for us to load environment variables and use the `.flaskenv` file.
112+
113+
Next, let's create the `.flaskenv` file:
114+
115+
```txt title=".flaskenv"
116+
FLASK_APP=app
117+
FLASK_ENV=development
118+
```
119+
120+
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.
121+
122+
The configuration that we'll do is to define the Flask app file (here, `app.py`). Then we'll also set the Flask environment to `development`, which does a couple things:
123+
124+
- Sets debug mode to true, which makes the app give us better error messages
125+
- Sets the app reloading to true, so the app restarts when we make code changes
126+
127+
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!
128+
129+
## Code improvements
130+
131+
### Creating a database file
132+
133+
First of all, let's move our "database" to another file.
134+
135+
Create a `db.py` file with the following content:
136+
137+
```py title="db.py"
138+
stores = {}
139+
items = {}
140+
```
141+
142+
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.
143+
144+
:::tip What is in each dictionary?
145+
Each dictionary will closely mimic how a database works: a mapping of ID to data. So each dictionary will be something like this:
146+
147+
```py
148+
{
149+
1: {
150+
"name": "Chair",
151+
"price": 17.99
152+
},
153+
2: {
154+
"name": "Table",
155+
"price": 180.50
156+
}
157+
}
158+
```
159+
160+
This will make it much easier to retrieve a specific store or item, just by knowing its ID.
161+
:::
162+
163+
Then, import the `stores` and `items` variables from `db.py` in `app.py`:
164+
165+
```py title="app.py"
166+
from db import stores, items
167+
```
168+
169+
## Using stores and items in our API
170+
171+
Now let's make use of stores and items separately in our API.
172+
173+
### `get_store`
174+
175+
Here are the changes we'll need to make:
176+
177+
<div className="codeTabContainer">
178+
<Tabs>
179+
<TabItem value="old" label="get_store (old)" default>
180+
181+
```py title="app.py"
182+
@app.get("/store/<string:name>")
183+
def get_store(name):
184+
for store in stores:
185+
if store["name"] == name:
186+
return store
187+
return {"message": "Store not found"}, 404
188+
```
189+
190+
</TabItem>
191+
<TabItem value="new" label="get_store (new)">
192+
193+
```py title="app.py"
194+
@app.get("/store/<string:store_id>")
195+
def get_store(store_id):
196+
try:
197+
# Here you might also want to add the items in this store
198+
# We'll do that later on in the course
199+
return stores[store_id]
200+
except KeyError:
201+
return {"message": "Store not found"}, 404
202+
```
203+
204+
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!
205+
206+
</TabItem>
207+
</Tabs>
208+
</div>
209+
210+
### `get_stores`
211+
212+
<div className="codeTabContainer">
213+
<Tabs>
214+
<TabItem value="old" label="get_stores (old)" default>
215+
216+
```py title="app.py"
217+
@app.get("/store")
218+
def get_stores():
219+
return {"stores": stores}
220+
```
221+
222+
</TabItem>
223+
<TabItem value="new" label="get_stores (new)">
224+
225+
```py title="app.py"
226+
@app.get("/store")
227+
def get_stores():
228+
return {"stores": list(stores.values())}
229+
```
230+
231+
</TabItem>
232+
</Tabs>
233+
</div>
234+
235+
### `create_store`
236+
237+
<div className="codeTabContainer">
238+
<Tabs>
239+
<TabItem value="old" label="create_store (old)" default>
240+
241+
```py title="app.py"
242+
@app.post("/store")
243+
def create_store():
244+
request_data = request.get_json()
245+
new_store = {"name": request_data["name"], "items": []}
246+
stores.append(new_store)
247+
return new_store, 201
248+
```
249+
250+
</TabItem>
251+
<TabItem value="new" label="create_store (new)">
252+
253+
```py title="app.py"
254+
@app.post("/store")
255+
def create_store():
256+
store_data = request.get_json()
257+
store_id = uuid.uuid4().hex
258+
store = {**store_data, "id": store_id}
259+
stores[store_id] = store
260+
261+
return store
262+
```
263+
264+
</TabItem>
265+
</Tabs>
266+
</div>
267+
268+
### `create_item`
269+
270+
<div className="codeTabContainer">
271+
<Tabs>
272+
<TabItem value="old" label="create_item (old)" default>
273+
274+
```py title="app.py"
275+
@app.post("/store/<string:name>/item")
276+
def create_item(name):
277+
request_data = request.get_json()
278+
for store in stores:
279+
if store["name"] == name:
280+
new_item = {"name": request_data["name"], "price": request_data["price"]}
281+
store["items"].append(new_item)
282+
return new_item, 201
283+
return {"message": "Store not found"}, 404
284+
```
285+
286+
</TabItem>
287+
<TabItem value="new" label="create_item (new)">
288+
289+
```py title="app.py"
290+
@app.post("/item")
291+
def create_item():
292+
item_data = request.get_json()
293+
if item_data["store_id"] not in stores:
294+
return {"message": "Store not found"}, 404
295+
296+
item_id = uuid.uuid4().hex
297+
item = {**item_data, "id": item_id}
298+
items[item_id] = item
299+
300+
return item
301+
```
302+
303+
Now we are POSTing to `/item` instead of `/store/<string:name>/item`. The endpoint will expect to receive JSON with `price`, `name`, and `store_id`.
304+
305+
</TabItem>
306+
</Tabs>
307+
</div>
308+
309+
310+
### `get_items` (new)
311+
312+
This is not an endpoint we could easily make when we were working with a single `stores` list!
313+
314+
```py
315+
@app.get("/item")
316+
def get_all_items():
317+
return {"items": list(items.values())}
318+
```
319+
320+
### `get_item_in_store`
321+
322+
<div className="codeTabContainer">
323+
<Tabs>
324+
<TabItem value="old" label="get_item_in_store (old)" default>
325+
326+
```py title="app.py"
327+
@app.get("/store/<string:name>/item")
328+
def get_item_in_store(name):
329+
for store in stores:
330+
if store["name"] == name:
331+
return {"items": store["items"]}
332+
return {"message": "Store not found"}, 404
333+
```
334+
335+
</TabItem>
336+
<TabItem value="new" label="get_item (new)">
337+
338+
```py title="app.py"
339+
@app.get("/item/<string:item_id>")
340+
def get_item(item_id):
341+
try:
342+
return items[item_id]
343+
except KeyError:
344+
return {"message": "Item not found"}, 404
345+
```
346+
347+
Now we are GETting from `/item` instead of `/store/<string:name>/item`. This is because while items are related to stores, they aren't inside a store anymore!
348+
349+
</TabItem>
350+
</Tabs>
351+
</div>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# In the course we run the app outside Docker
2+
# until lecture 5.
3+
FROM python:3.10
4+
EXPOSE 5000
5+
WORKDIR /app
6+
RUN pip install flask
7+
COPY . .
8+
CMD ["flask", "run", "--host", "0.0.0.0"]

0 commit comments

Comments
 (0)