.. _lab1:
Lab 1: Back end (Python)
========================
I denna laboration kommer vi bygga en enkel webbserver med hjälp av
Python och ramverket
`Flask `__. Vi kommer även
introducera koncepten REST och JSON samt skapa en databas. Med hjälp av
detta kommer ni skapa en server innehållande ett API som kommunicerar
med JSON över HTTP och följer REST-stilen. Ert API ska i slutändan
innehålla följande routes:
- GET, POST /cars
- GET, PUT, DELETE /cars/``[int:car_id]``
- GET, POST /customers
- GET, PUT, DELETE /customers/``[int:customer_id]``
- GET /customers/``[int:customer_id]``/cars
Inledningsvis kommer vi använda `Postman `__
för att hantera HTTP-kommunikation till och från vår server.
Kom ihåg att kontinuerligt göra git commits; ett rekommenderat lägsta
intervall är att skapa en commit för varje del i labben.
HTTP (Hypertext Transfer Protocol)
----------------------------------
Innan vi börjar kommer vi behöva veta mer om kommunikation över
internet och specifikt hur detta går till via HTTP. Läs minst igenom
följande
`artikel `__ men vi
uppmuntrar till att hitta egna artiklar relaterade till HTTP om
artikeln ovan inte skapat grundläggande förståelse. Det är viktigt att
förstå:
- De olika metoderna GET, POST, PUT och DELETE samt hur de används.
- Headers (Specifikt inklusive “content-type”)
- Body
- Statuskoder
Postman
-------
I syfte att förenkla processen med att skicka och ta emot
HTTP-meddelanden kommer vi använda ett program som hjälper oss med
detta. Ladda ner och installera Postman från `denna
sida `__ och läs sedan igenom en
`artikel `__ som går
igenom hur Postman kan användas.
REST
----
Det finns olika sätt att strukturera kommunikation med en webbserver,
men den stil vi använder i denna kurs kallas REST och står för
Representational State Transfer. REST innebär i korta drag att man
definierar resurser (exempelvis en “User”) som kan nås via specifika
URIs. En URI i kombination med någon utav HTTP-metoderna (GET, POST,
PUT, DELETE) utgör ett kommando. Läs mer om
`REST `__
och skumma igenom `dokumentationen `__.
JSON
----
Data kan struktureras på olika sätt, vi använder
`JSON-formatet `__ eftersom det är
enkelt att förstå samt smidigt att hantera med både JavaScript och
Python.
Förberedande frågor
-------------------
Svara på följande frågor och skriv ner era svar innan demonstration:
1. För vilka typer av operationer ska de olika HTTP-verben anvädas? Ge ett
konkret exempel på lämplig funktionalitet för respektive verb.
2. Ge exempel på minst fyra statuskoder samt när de bör användas.
3. I vilken del av en HTTP-request kan data i JSON-format hittas?
Del 1: Routing
--------------
1. Börja med att följa :ref:`start_new_lab` för att skapa
en ny branch för laborationen med namn ``lab1``.
Notera att det går att starta labben innan den tidigare är godkänd.
2. Skapa routen /cars i er main.py som returnerar en lista med bilar i
JSON-format. Listan med bilar kan tillsvidare vara hårdkodad enligt
följande Python-kod:
.. code:: python
car_list = [
{"id": 1, "make": "Volvo", "model": "V70"},
{"id": 2, "make": "SAAB", "model": "95"}
]
En route definieras genom att definiera en metod som dekoreras med
``app.route``.
.. code-block :: python
:caption: Ett exempel på en passande deklaration för /cars kan se ut så här
@app.route('/cars')
def cars():
# ...
.. tip ::
Python-dictionaries och vanliga datatyper kan konverteras
automatiskt till JSON-format av Flask, andra datatyper kan
konverteras genom att använda
`jsonify `__.
Notera att listor inte får finnas på topp-nivån i det som returneras, men ``jsonify`` kan användas för att returnera listor.
.. tip ::
Ni behöver starta om Flask-applikationen när ni har gjort
ändringar i koden för att de ska appliceras. Om ni vill att servern
automatiskt ska ladda ändringarna när filerna sparas, skicka in
``debug=True`` som parameter till app.run().
När ni anropar servern från Postman ska svaret se ut som följande:
|lab1-1-13|
Om det inte finns någon bil ska en tom lista presenteras.
3. | Skapa en route /cars med en parameter för att hämta en specifik bil
ur listan car_list baserat på dess id. Denna route ska ha följande
format: ``/cars/[int:car_id]``
.. tip ::
Information om `routes och parametrar i
Flask `__.
Om ni inte inkluderar information om vilken typ parametern ska ha så
förutsätter Flask att den har typen string (och detta kan ge följdfel).
Om den efterfrågade bilen hittas ska svaret från servern se ut så
här:
|lab1-1-2| Ni ska även inkludera enklare felhantering. I det här
fallet innebär det att om en bil inte kan hittas ska HTTP-statuskoden
404 Not Found visas för att indikera att resursen inte kunde hittas.
Svaret ska då se hur så här:
.. figure:: images/lab1/lab1-1-21.png
.. tip ::
För att svara med endast en HTTP-statuskod i Flask kan ni
använda metoden abort(), se följande
`artikel `__
för mer information och exempel.
Del 2: Databas
--------------
I de flesta webbapplikationer behövs något sätt att spara data över tid.
Oftast hanteras detta genom att använda en databas. I denna laboration
ska vi använda databasen `SQLite `__.
SQLite är en databas som är väldigt enkel att komma igång med, men bör
ej användas på detta sätt i en produktionsmiljö. För att enklare kunna
hantera databasen ska vi också använda
`SQLAlchemy `__.
SQLAlchemy innehåller en så kallad ORM (Object Relational Mapper) som
hjälper oss att mappa våra framtida Python-klasser mot databastabeller.
Mer djupgående dokumentation om SQLAlchemy finns
`här `__.
Vi ska nu ordna så att bilar sparas i en databas istället för att bestå
av en hårdkodad Python-lista.
1. Installera Flask-SQLAlchemy (SQLAlchemy + integration mot Flask)
genom att köra följande kommando (i er virtuella Python-miljö):
``(venv)$ pip install flask_sqlalchemy``
.. tip ::
För att enklare hantera paket i framtiden kan ni skapa en fil,
server/requirements.txt, med följande innehåll:
::
Flask==2.0.2
Flask-SQLAlchemy==2.5.1
SQLAlchemy==1.4.31
Sedan är det bara att köra följande för att installera alla paket
på en gång:
.. code :: bash
(venv)$ pip install -r requirements.txt
Det går även att använda pip för att automatiskt skapa denna fil. Detta
görs med kommandot:
.. code ::
(venv)$ pip freeze >> requirements.txt
2. Lägg till följande konfiguration under tilldelningen av variabeln app
i main.py:
.. code :: python
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
Ni behöver även importera SQLAlchemy:
.. code :: python
from flask_sqlalchemy import SQLAlchemy
Databasen kommer sedan bestå av filen database.db vilken vi inte
vill versionshantera. Lägg därför till i er ``.gitignore``:
.. code :: bash
# Database
server/database.db
3. Det är nu dags att skapa vår första modell, “Car”. En modell skapas
genom att skapa en klass som ärver av db.Model enligt följande:
.. code :: python
class Car(db.Model):
id = db.Column(db.Integer, primary_key=True)
make = db.Column(db.String, nullable=False)
model = db.Column(db.String, nullable=False)
def __repr__(self):
return ' SELECT * FROM car;
id make model
------- -------- ----------
1 Volvo V70
2 SAAB 95
4. Ta nu bort era hårdkodade bilar, d.v.s. ``cars_list``, helt och
hållet.
5. Det är nu dags att testa att manipulera databasen. Vi börjar med att
göra det genom Python-interpretatorn:
Starta python, med er virtuella miljö aktiverad:
.. code :: bash
(venv) ~/tddd83-labs/server$ python
Python 3.8.10 (default, Nov 26 2021, 20:14:08)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
Importera nu db och er nya modell, Car, från main.py genom att
skriva:
.. code :: python
>>> from main import db, Car
Låt SQLAlchemy skapa tabeller i databasen genom att köra följande:
.. code :: python
>>> db.create_all()
För att spara en ny bil i databasen skapar ni helt enkelt en instans
av Car och sparar den i databasen så här:
.. code :: python
>>> volvo = Car(make="Volvo", model="V70")
>>> db.session.add(volvo)
>>> db.session.commit()
Fältet id sköts automatiskt eftersom det är definierat som en
*primary key*.
För att sedan hämta alla bilar i databasen använder ni exempelvis:
.. code :: python
>>> Car.query.all()
[`__
för SQLAlchemy.
6. Nu ska ni äntligen få använda databasen när ert API svarar på
HTTP-anrop! Skriv om era metoder för /cars och /cars/``[int:car_id]``
så att de fungerar precis som tidigare, men hämtar all data från
databasen.
.. tip ::
Tänk på att jsonify inte kan hantera godtyckliga datatyper
och att ni på något sätt behöver konvertera en Car-instans till en
Python-dictionary för att den sedan ska kunna konverteras till JSON.
En bra början kan vara att lägga till en metod i Car-klassen så att
ni får något i stil med detta:
.. code :: python
class Car(db.Model):
id = db.Column(db.Integer, primary_key=True)
make = db.Column(db.String, nullable=False)
model = db.Column(db.String, nullable=False)
def __repr__(self):
return '`__.
.. tip ::
Att skilja på HTTP-metoderna kan göras enligt
.. code :: python
@app.route('/cars', methods=['GET', 'POST'])
def cars():
if request.method == 'GET':
# Handle GET request
elif request.method == 'POST':
# Handle POST request
(``request`` importeras från flask) eller genom att skapa en separat
metod som endast hanterar exempelvis POST.
.. tip ::
För att få ut JSON-datan från förfrågningen kan ni
exempelvis använda request.get_json(). Headern *Content-Type* måste
vara satt till *application/json*.
`Här `__
kan ni läsa mer om detta.
2. Uppdatera metoden för /cars/``[int:car_id]`` så att den även stödjer
uppdatering via PUT. Om en bil med efterfrågat id inte hittas ska ni
svara med statuskoden 404 (Not Found).
Vid PUT ska ni ta in ett JSON-objekt som representerar en bil eller
delar av en bil och uppdatera de fält i databasen som finns angivna i
det inkommande JSON-objektet. Svaret ska bestå av en uppdaterad bil.
Exempelvis:
.. code :: text
REQUEST:
PUT /cars/1
{
"make": "BMW"
}
RESPONSE:
200 OK
{
"id": 1,
"make": "BMW",
"model": "A6"
}
och:
.. code :: text
REQUEST: PUT /cars/1
{ “id”: 1234, “make”: “Volvo”, “model”: “V70” }
RESPONSE: 200 OK
{ "id”: 1, “make”: “Volvo”, “model”: “V70” }
Tänk på att användaren inte ska kunna modifiera attributet id.
3. Implementera funktionalitet för DELETE /cars/``[int:car_id]`` för
borttagning av bilar. Om en bil med efterfrågat id inte hittas ska ni
svara med statuskoden 404 (Not Found).
Om bilen hittas ska den tas bort ur databasen och svaret ska endast
innehålla statuskoden 200 (Success), ingen ytterligare data behövs.
Del 4: Kund-modell och relationer
---------------------------------
Med hjälp av databasen och modeller kan relationer mellan olika typer av
objekt beskrivas. Ni ska nu utöka systemet med en kund-modell som har en
relation till bil-modellen. En bil ska kunna ha 0 eller 1 en kund åt
gången, men en kund ska kunna ha 0 eller fler bilar
(*One-to-Many*-relation, *1->N*).
1. Skapa en modell Customer som innehåller minst datan *id*, *name* och
*email* på samma sätt som bil-modellen.
2. Modifiera bil-modellen samt kund-modellen så att de har en sådan
relation som beskrivs ovan (1->N). Tänk på att en kund inte
nödvändigtvis alltid måste ha en relation till en bil.
.. tip ::
Läs om
`relationer `__
i databaser.
3. För att era ändringar ska återspeglas i databasen behöver ni berätta
för SQLAlchemy att utföra ändringarna. Det enklaste sättet är helt
enkelt att tömma databasen och skapa den på nytt. öppna
Python-interpretatorn och utför följande:
.. code :: text
(venv) ~/tddd83-labs/server$ python
Python 3.8.10 (default, Nov 26 2021, 20:14:08)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
.. code :: python
>>> from main import db
>>> db.drop_all()
>>> db.create_all()
Observera att all data kommer att tas bort! Detta tillvägagångssätt
är ej rekommenderat till annat än enkelt testning/utveckling. För att
utföra ändringar i databaser där datan ej kan förloras kan
förändringar exempelvis beskrivas i form av *migrations*. Se `exempel
1 `__ eller `exempel
2 `__ om ni vill
hitta ett passande verktyg för detta ändamål.
4. Skapa nu, på motsvarande sätt som tidigare, funktionalitet för
- GET, POST /customers
- GET, PUT, DELETE /customers/``[int:customer_id]``
5. Uppdatera funktionaliteten för GET /cars så att en eventuella kunder
också visas i svaret, d.v.s:
::
REQUEST:
GET /cars
RESPONSE:
200 OK
[
{
"id": 1,
"make": "Volvo",
"model": "V70",
"customer": {
"id": 1,
"name": "Test Person",
"email": "test@test.t"
}
},
{
"id": 2,
"make": "Audi",
"model": "A8",
"customer": null
}
...
]
Testa detta genom att skapa en kund och sedan sätta en relation till
en bil med hjälp av Python-interpretatorn (se del 2 punkt 5 ovan) och
låta en bil vara utan kund.
6. Uppdatera funktionaliteten för GET /cars/``[int:car_id]`` så att en
eventuell kund också visas i svaret. Svaret från ert API ska alltså
se ut så här ifall en bil har en relation till en kund:
.. figure:: images/lab1/lab1-4-2.png
7. Uppdatera funktionaliteten för PUT /cars/``[int:car_id]`` så att även
attributet för kund-relationen kan uppdateras, d.v.s:
::
REQUEST:
PUT /cars/2
{
"customer_id": 1
}
Svaret från servern ska se ut som för GET /cars/``[int:car_id]``
ovan, med den nya kunden. Ifall det inte finns en kund med det id som
angivits går det bra att lämna relationen orörd och svara med
statuskoden 404.
8. Lägg till funktionalitet för GET
/customer/``[int:customer_id]``/cars. Er server ska svara med en
lista över bilar som har en relation till kunden i fråga. d.v.s:
::
[
{
"id": 1,
"make": "Volvo",
"model": "V70",
},
{
"id": 2,
"make": "Audi",
"model": "A8",
}
]
Om kunden inte har någon relation till någon bil ska en tom lista
presenteras.
9. Sist men inte minst, uppdatera funktionaliteten för borttagning av
kunder så att relationerna hanteras. Om en kund tas bort ska
eventuella befintliga relationer också nollställas.
Redovisning
-----------
Se :ref:`redovisning`.
.. |lab1-1-12| image:: images/lab1/lab1-1-12.png
.. |lab1-1-13| image:: images/lab1/lab1-1-13.png
.. |lab1-1-2| image:: images/lab1/lab1-1-2.png
.. |lab1-2-21| image:: images/lab1/lab1-2-21.png
.. |lab1-2-22| image:: images/lab1/lab1-2-22.png