TDDD83 Kandidatprojekt datateknik
Laboration 1
# Back end (Python)
I denna laboration kommer vi bygga en enkel webbserver med hjälp av Python
och ramverket [Flask](https://flask.palletsprojects.com/en/1.1.x/).
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](https://www.getpostman.com)
för att hantera HTTP-kommunikation till och från vår server.
__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](http://www.steves-internet-guide.com/http-basics) 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](https://www.getpostman.com) och läs
sedan igenom en [artikel](https://www.guru99.com/postman-tutorial.html#1)
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](https://sv.wikipedia.org/wiki/Representational_State_Transfer) och
skumma igenom [dokumentationen](https://restfulapi.net).
__JSON__
Data kan struktureras på olika sätt, vi använder
[JSON-formatet](https://www.json.org/json-sv.html) 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. Följ [instruktionerna om "Påbörjande av ny labb"](inlamning-och-git#p%E5b%F6rjande-av-ny-labb) för att skapa en ny branch för laborationen med namn `lab1`
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 (notera att topp-nivÃ¥n inte längre fÃ¥r vara en lista; bilderna nedan är frÃ¥n en tidigare version av Flask):
```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`. Ett
exempel på en passande deklaration för /cars kan se ut så här:

__Tips:__ Python-dictionaries och vanliga datatyper (dock ej listor som returneras direkt!) kan konverteras automatiskt till
JSON-format av Flask, andra datatyper kan konverteras genom att använda
[jsonify](https://flask.palletsprojects.com/en/1.1.x/quickstart/#apis-with-json). jsonify klarar listor.
__Tips:__ 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:

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]
```
__Tips:__ Information om [routes och parametrar i Flask](https://flask.palletsprojects.com/en/1.1.x/quickstart/#routing).
Om den efterfrågade bilen hittas ska svaret från servern se ut så här:

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:

__Tips:__ För att svara med endast en HTTP-statuskod i Flask kan ni använda metoden
abort(), se följande [artikel](https://flask.palletsprojects.com/en/1.1.x/quickstart/#redirects-and-errors) 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](https://www.sqlite.org/index.html). 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](https://flask-sqlalchemy.palletsprojects.com/en/2.x/). 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](https://docs.sqlalchemy.org/en/13/).
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
```
__Tips:__ För att enklare hantera paket i framtiden kan ni skapa en fil,
server/requirements.txt, med följande innehåll:
```
Flask==1.1.1
Flask-SQLAlchemy==2.5.1
```
Sedan är det bara att köra följande för att installera alla paket på en gång:
```
(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:
2. Lägg till följande konfiguration under tilldelningen av variabeln app i main.py:  Ni behöver även importera SQLAlchemy:  Databasen kommer sedan bestå av filen database.db vilken vi inte vill versionshantera. Lägg därför till  i er .gitignore-fil och gör en commit med endast den filen. 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:  Som visas i exemplet kan en modell, precis som vanligt, innehålla godtyckliga metoder och attribut precis som en vanlig klass i Python. Tack vare att den ärver av SQLAlchemys db.Model finns dessutom många metoder tillgängliga automatiskt i klassen, exempelvis `Car.query.all()` som returnerar alla bil-instanser i databasen. `db.Column` indikerar att attributet ska mappas mot en kolumn i databasen. Modellen ovan kommer att representeras av en tabell i databasen med fälten id, make samt model, som visas i följande bild.  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: ``` (venv) $ python ``` Ni ska nu se något i den här stilen:  Importera nu db och er nya modell, Car, från main.py genom att skriva:  Låt SQLAlchemy skapa tabeller i databasen genom att köra följande:  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:  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:  Mer information om hur ni hämtar, lägger till samt tar bort data hittar ni i [dokumentationen](https://flask-sqlalchemy.palletsprojects.com/en/2.x/queries/) 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. __Tips:__ 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:  ## Del 3: Stöd för fler HTTP-metoder Att visa data på förfrågning är visserligen väldigt bra, men det är ju också väldigt praktiskt om användarna kan lägga till, uppdatera och ta bort data själva via ert API! För att möjliggöra detta ska ni nu få implementera funktionalitet för att även stödja HTTP-metoderna POST, PUT och DELETE. 1. Utöka metoden för routen /cars så att den även accepterar POST-anrop. Vid GET ska metoden fungera precis som tidigare. Vid POST ska en ny bil läggas till i databasen med det märke och den modell som angetts i anropet. Anropet ska, i Postman, se ut såhär:  Svaret till POST-anropet ska bestå av det nya bil-objektet (på samma sätt som /cars/`[int:car_id]`), d.v.s: ``` { "id": 1, "make": "Audi", "model": "A6" } ``` Läs om hur ni hanterar olika [HTTP-metoder och indata](https://flask.palletsprojects.com/en/1.1.x/quickstart/#the-request-object). __Tips:__ Att skilja på HTTP-metoderna kan göras enligt  (_request_ importeras från flask) eller genom att skapa en separat metod som endast hanterar exempelvis POST. __Tips:__ 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](https://flask.palletsprojects.com/en/1.1.x/api/#flask.Request.get_json) 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: ``` REQUEST: PUT /cars/1 { "make": "BMW" } RESPONSE: 200 OK { "id": 1, "make": "BMW", "model": "A6" } ``` och ``` 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. __Tips:__ Läs om [relationer](https://flask-sqlalchemy.palletsprojects.com/en/2.x/models/#one-to-many-relationships) 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:  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](https://pypi.org/project/alembic/) eller [exempel 2](https://flask-migrate.readthedocs.io/en/latest/) 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:  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: Följ [instruktionerna för "Redovisning"](inlamning-och-git#redovisning).
(venv)$ pip freeze >> requirements.txt
2. Lägg till följande konfiguration under tilldelningen av variabeln app i main.py:  Ni behöver även importera SQLAlchemy:  Databasen kommer sedan bestå av filen database.db vilken vi inte vill versionshantera. Lägg därför till  i er .gitignore-fil och gör en commit med endast den filen. 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:  Som visas i exemplet kan en modell, precis som vanligt, innehålla godtyckliga metoder och attribut precis som en vanlig klass i Python. Tack vare att den ärver av SQLAlchemys db.Model finns dessutom många metoder tillgängliga automatiskt i klassen, exempelvis `Car.query.all()` som returnerar alla bil-instanser i databasen. `db.Column` indikerar att attributet ska mappas mot en kolumn i databasen. Modellen ovan kommer att representeras av en tabell i databasen med fälten id, make samt model, som visas i följande bild.  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: ``` (venv) $ python ``` Ni ska nu se något i den här stilen:  Importera nu db och er nya modell, Car, från main.py genom att skriva:  Låt SQLAlchemy skapa tabeller i databasen genom att köra följande:  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:  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:  Mer information om hur ni hämtar, lägger till samt tar bort data hittar ni i [dokumentationen](https://flask-sqlalchemy.palletsprojects.com/en/2.x/queries/) 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. __Tips:__ 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:  ## Del 3: Stöd för fler HTTP-metoder Att visa data på förfrågning är visserligen väldigt bra, men det är ju också väldigt praktiskt om användarna kan lägga till, uppdatera och ta bort data själva via ert API! För att möjliggöra detta ska ni nu få implementera funktionalitet för att även stödja HTTP-metoderna POST, PUT och DELETE. 1. Utöka metoden för routen /cars så att den även accepterar POST-anrop. Vid GET ska metoden fungera precis som tidigare. Vid POST ska en ny bil läggas till i databasen med det märke och den modell som angetts i anropet. Anropet ska, i Postman, se ut såhär:  Svaret till POST-anropet ska bestå av det nya bil-objektet (på samma sätt som /cars/`[int:car_id]`), d.v.s: ``` { "id": 1, "make": "Audi", "model": "A6" } ``` Läs om hur ni hanterar olika [HTTP-metoder och indata](https://flask.palletsprojects.com/en/1.1.x/quickstart/#the-request-object). __Tips:__ Att skilja på HTTP-metoderna kan göras enligt  (_request_ importeras från flask) eller genom att skapa en separat metod som endast hanterar exempelvis POST. __Tips:__ 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](https://flask.palletsprojects.com/en/1.1.x/api/#flask.Request.get_json) 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: ``` REQUEST: PUT /cars/1 { "make": "BMW" } RESPONSE: 200 OK { "id": 1, "make": "BMW", "model": "A6" } ``` och ``` 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. __Tips:__ Läs om [relationer](https://flask-sqlalchemy.palletsprojects.com/en/2.x/models/#one-to-many-relationships) 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:  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](https://pypi.org/project/alembic/) eller [exempel 2](https://flask-migrate.readthedocs.io/en/latest/) 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:  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: Följ [instruktionerna för "Redovisning"](inlamning-och-git#redovisning).
Sidansvarig: Martin Sjölund
Senast uppdaterad: 2022-01-27