* Deadline hard: 14 aprilie 2024, ora 23:55. Veți primi o depunctare de 10% din punctajul maxim al temei pentru fiecare zi de întârziere, până la maxim 7 zile, adică până pe 14 aprilie 2024, ora 23:55.
În cadrul acestei teme veți avea de implementat un server python care va gestiona o serie de requesturi plecând de la un set de date în format *csv* (comma separated values). Serverul va oferi statistici pe baza datelor din csv.
Setul de date conține informații despre nutriție, activitatea fizică și obezitate în Statele Unite ale Americii în perioada 2011 - 2022. Datele au fost colectate de către U.S. Department of Health & Human Services. Informațiile sunt colectate per stat american (ex. California, Utah, New York) și răspund următorului set de întrebări:
Valorile pe care le veți folosi în calculul diverselor statistici la care răspunde aplicația voastră se găsesc în coloana Data_Value.
Aplicația server pe care o dezvoltați este una multi-threaded. Atunci când serverul este pornit, trebuie să încărcați fișierul csv și să extrageți informațiile din el a.î. să puteți calcula statisticile cerute la nivel de request.
Întrucât procesarea datelor din csv poate dura mai mult timp, modelul implementat de către server va fi următorul: * un endpoit (ex. '/api/states_mean') care primește requestul și va întoarce clientului un job_id (ex. “job_id_1”, “job_id_2”, …, “job_id_n”) * endpointul '/api/get_results/job_id' care va verifica dacă job_id-ul este valid, rezultatul calculului este gata sau nu și va returna un răspuns corespunzător (detalii mai jos)
Asociază un job_id requestului, pune jobul (closure care încalsulează unitatea de lucru) într-o coadă de joburi care este procesată de către un Thread pool, incrementează job_id-ul intern și returnează clientului job_id-ul asociat.
Un thread va prelua un job din coada de joburi, va efectua operația asociată (ceea ce a fost capturat de către closure) și va scrie rezultatul calculului într-un fișier cu numele job_id-ului în directorul results/.
Primește o întrebare (din setul de întrebări de mai sus) și calculează media valorilor înregistrate (Data_Value) din intervalul total de timp (2011 - 2022) pentru fiecare stat, și sortează crescător după medie.
Primește o întrebare (din setul de întrebări de mai sus) și un stat, și calculează media valorilor înregistrate (Data_Value) din intervalul total de timp (2011 - 2022).
Primește o întrebare (din setul de întrebări de mai sus) și calculează media valorilor înregistrate (Data_Value) din intervalul total de timp (2011 - 2022) și întoarce primele 5 state.
Primește o întrebare (din setul de întrebări de mai sus) și calculează media valorilor înregistrate (Data_Value) din intervalul total de timp (2011 - 2022) și întoarce ultimele 5 state.
Primește o întrebare (din setul de întrebări de mai sus) și calculează media valorilor înregistrate (Data_Value) din intervalul total de timp (2011 - 2022) din întregul set de date.
Primește o întrebare (din setul de întrebări de mai sus) și calculează diferența dintre global_mean și state_mean pentru toate statele.
Primește o întrebare (din setul de întrebări de mai sus) și un stat, și calculează diferența dintre global_mean și state_mean pentru statul respectiv.
Primește o întrebare (din setul de întrebări de mai sus) și calculează valoarea medie pentru fiecare segment (Stratification1) din categoriile (StratificationCategory1) fiecărui stat.
Primește o întrebare (din setul de întrebări de mai sus) și un stat, și calculează valoarea medie pentru fiecare segment (Stratification1) din categoriile (StratificationCategory1).
Răspunde la un apel de tipul GET și va duce la notificarea Thread Poolului despre încheierea procesării. Scopul acesteia este de a închide aplicația într-un mod graceful: nu se mai acceptă requesturi noi, se termină de procesat requesturile înregistrate până în acel moment (drain mode) și apoi aplicația poate fi oprită.
Răspunde la un apel de tipul GET cu un JSON care conține toate JOB_ID-urile de până la acel moment și statusul lor. De exemplu:
{ "status": "done" "data": [ { "job_id_1": "done"}, { "job_id_2": "running"}, { "job_id_3": "running"} ] }
Răspunde la un apel de tipul GET cu numărul joburilor rămase de procesat. După un /api/graceful_shutdown și o perioadă de timp, aceasta ar trebui să întoarcă valoarea 0, semnalând astfel că serverul flask poate fi oprit.
Răspunde la un apel de tipul GET (job_id-ul este parte din URL). Acesta verifică dacă job_id-ul primit este valid și răspunde cu un JSON corespunzător, după cum urmează:
1. JOB_ID-ul este invalid
{ "status": "error", "reason": "Invalid job_id" }
2. JOB_ID-ul este valid, dar rezultatul procesării nu este gata
{ "status": "running", }
3. JOB_ID-ul este valid și rezultatul procesării este gata
{ "status": "done", "data": <JSON_REZULTAT_PROCESARE> }
Implementarea serverului se face folosind framework-ul flask și va extinde scheletul de cod oferit. Mai multe detalii despre Flask găsiți mai jos. Deasemeni, un tutorial extensiv (pe care vi-l recomandăm) este The flask mega tutorial.
Python Flask este un micro-framework web open-source care permite dezvoltatorilor să creeze aplicații web ușor și rapid, folosind limbajul de programare Python. Flask este minimalist și flexibil, oferind un set de instrumente de bază pentru crearea unei aplicații web, cum ar fi rutele URL, gestionarea cererilor și a sesiunilor, șablonarea și gestionarea cookie-urilor. Cu Flask, dezvoltatorii pot construi rapid API-uri sau aplicații web de dimensiuni mici și medii.
Pentru a instala Flask, creați-vă un mediu virtual (pentru a nu instala pachete global, pe sistem) folosind comanda
$ python -m venv venv
Activați mediul virtual
$ source venv/bin/activate
Și instalați pachetele din fișierul requirements.txt
$ python -m pip install -r requirements.txt
Pașii de creare a mediului virtual și de instalare a pachetelor se regăsesc în fișierul Makefile.
Astfel, pentru a vă crea spațiul de lucru, rulați următoarele comenzi în interpretorul vostru de comenzi (verificat în bash
și zsh
)
make create_venv source venv/bin/activate make install
O rută în cadrul unei aplicații web, cum ar fi în Flask, reprezintă un URL (Uniform Resource Locator) specific către care aplicația web va răspunde cu un anumit conținut sau funcționalitate. Atunci când un client (de obicei un browser web) face o cerere către serverul web care găzduiește aplicația Flask, ruta determină ce cod va fi executat și ce răspuns va fi returnat clientului. În Flask, rutele sunt definite folosind decoratori care leagă funcții Python de URL-uri specifice, permitând astfel aplicației să răspundă în mod dinamic la cereri (requesturi).
În Flask, puteți defini o rută care răspunde la un apel de tip GET folosind decoratorul @app.route() și specificând metoda *HTTP* (methods=['GET']). Pentru a răspunde la un apel de tipul POST (apel folosit pentru a trimite date de către un client către server) folosim același decorator și specificăm methods=['POST']. De exemplu:
from flask import request @app.route('/', methods=['GET']) def index(): return 'Aceasta este o rută care răspunde la un apel de tip GET' @app.route('/post', methods=['POST']) def post_route(): data = request.json # Se obțin datele JSON trimise prin POST return 'Aceasta este o rută care răspunde la un apel de tip POST'
În cazul API-urilor este un best practice ca datele returnate să fie în format JSON, pentru a fi ușor de prelucrat de către alte servicii în mod programatic. Pentru a returna un obiect JSON în Flask, vom folosi helperul jsonify() ca în exemplul de mai jos:
from flask import request, jsonify @webserver.route('/api/post_endpoint', methods=['POST']) def post_endpoint(): if request.method == 'POST': # Presupunem că metoda conține date JSON data = request.json print(f"got data in post {data}") # Procesăm datele primite # Pentru exemplu, vom returna datele primite response = {"message": "Received data successfully", "data": data} return jsonify(response) else: # Nu acceptăm o altă metodă return jsonify({"error": "Method not allowed"}), 405
Interacțiunea cu serverul se va face pe bază de mesaje JSON, după cum este descris mai jos. Vă recomandăm să vă uitați în suita de teste, în directoarele input și output pentru a vedea informațiile mult mai detaliat.
Un input pentru un request care primește doar o întrebare în următorul format:
{ "question": "Percent of adults aged 18 years and older who have an overweight classification" }
Unul care așteaptă o întrebare și un stat are următorul format:
{ "question": "Percent of adults who engage in no leisure-time physical activity", "state": "South Carolina" }
Un răspuns JSON va avea mereu structura:
{ "status": "done", "data": <JSON_REZULTAT_PROCESARE> }
JSON_REZULTAT_PROCESARE este un obiect JSON așa cum se regăsește în directorul output, pentru fiecare endpoint din directorul tests.
Testarea se va realiza folosind atât unitteste, cât și teste funcționale.
Pentru a rula testele, folosiți fișierul Makefile
.
Într-un shell 1) activați mediul virtual și 2) porniți serverul
source venv/bin/activate make run_server
Într-un alt shell 1) activați mediul virtual și 2) porniți checkerul
source venv/bin/activate make run_tests
source venv/bin/activate
Dacă nu ați activat mediul virtual, make
vă va arunca următoarea eroare (linia, ex 8, poate să difere).
Makefile:8: *** "You must activate your virtual environment. Exiting...". Stop.
Pentru testarea funcțiilor din server veți folosi modulul de unittesting al limbajului Python.
Pentru a testa comportamentul definiți în fișierul unittests/TestWebserver.py
o clasă de testare numită TestWebserver
.
Clasa TestWebserver
va testa funcționalitatea tuturor rutelor definite de voi.
Dacă definiți alte metode, va trebui să adăugați teste și pentru acestea.
Vă recomandăm să folosiți metoda setUp pentru a inițializa o instanță a clasei testate și orice altceva ce vă ajută în testarea codului.
Un exemplu de utilizare a metodei setUp
este disponibil în documentație.
Vrem să utilizăm fișiere de logging în aplicațiile pe care le dezvoltăm pentru a putea urmări flowul acestora a.î. să ne ajute în procesul de debug.
Folosind modulul de logging, trebuie să implementați un fișier de log, numit “webserver.log”, în care veți urmări comportamentul serverului.
În fișierul de log veți nota, folosind nivelul info()
, toate intrările și ieșirile în/din rutele implementate.
În cazul metodelor care au parametrii de intrare, informația afișată la intrarea în funcție va afișa și valorile parametrilor.
Fișierul va fi implementat folosind RotatingFileHandler: astfel se poate specifica o dimensiune maximă a fișierului de log și un număr maxim de copii istorice. RotatingFileHandler ne permite să ținem un istoric al logurilor, fișierele fiind stocate sub forma “file.log”, “file.log.1”, “file.log.2”, … “file.log.max”.
Vă încurajăm să folosiți fișierul de log și pentru a înregistra erori detectate.
În mod implicit, timestamp-ul logurilor folosește timpul mașinii pe care rulează aplicația (local time). Acest lucru nu este de dorit în practică deoarece nu putem compara loguri de pe mașini aflate în zone geografice diferite. Din acest motiv, timestampul este ținut în format UTC/GMT. Asigurați-vă că folosiți gmtime, și nu localtime. Pentru aceasta trebuie să folosiți metoda formatTime.
O descriere completă a cum puteți utiliza modului de logging este prezentă în categoria HOWTO a documentației.
Arhiva temei va fi încărcată pe moodle
Arhiva (fişier .zip) trebuie să conțină:
.py
folosite în dezvoltare ()README
git-log
(Îl obțineți rulând comanda git log > git-log
)api_server.py app/ app/routes.py app/task_runner.py app/data_ingestor.py app/__init__.py README unittests/ unittests/mytests.py git-log
tests
.
Tema se va implementa Python>=3.7.
Notarea va consta în 80 pct acordate egale între testele funcționale, 10 pct acordate pentru unitteste și 10 pct acordate pentru fișierul de logging. Depunctări posibile sunt:
Se acordă bonus 5 pct pentru adăugarea directorului .git
și utilizarea versionării în cadrul repository-ului.
Vom testa sursele voastre cu pylint configurat conform fișierului pylintrc
din cadrul repo-ului dedicat temei. Atenție, rulăm pylint doar pe modulele completate și adăugate de voi, nu și pe cele ale testerului.
Deoarece apar diferențe de scor între versiuni diferite de pylint, vom testa temele doar cu ultima versiune. Vă recomandăm să o folosiți și voi tot pe aceasta.
Vom face depunctări de până la -5pct dacă verificarea făcută cu pylint vă dă un scor mai mic de 8.
Pentru a clona repo-ul și a accesa resursele temei 1:
student@asc:~$ git clone https://gitlab.cs.pub.ro/asc/asc-public.git student@asc:~$ cd asc/assignments student@asc:~/assignments$ cd 1-le_stats_sportif
Pentru întrebări sau nelămuriri legate de temă folosiți forumul temei.
ATENȚIE să nu postați imagini cu părți din soluția voastră pe forumul pus la dispoziție sau orice alt canal public de comunicație. Dacă veți face acest lucru, vă asumați răspunderea dacă veți primi copiat pe temă.