Faffio commited on
Commit
bcf1c8c
·
1 Parent(s): 04a4266

Add CI Pipeline with Tests and Docker build

Browse files
.github/workflows/ci_papeline.yaml ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: MLOps CI Pipeline
2
+
3
+ # 1. TRIGGER: Quando deve partire questa pipeline?
4
+ # Parte su ogni "push" sul ramo main e su ogni "pull request"
5
+ on:
6
+ push:
7
+ branches: [ "main" ]
8
+ pull_request:
9
+ branches: [ "main" ]
10
+
11
+ # Appena attivato il trigger, viene avviato build docker sotto che richieste l'esecuzione di run_test, se ok parte la creazione dell'immagine docker
12
+ jobs:
13
+ # JOB 1: Esecuzione dei Test Automatici
14
+ run_tests:
15
+ runs-on: ubuntu-latest # Usa una macchina Linux di GitHub
16
+
17
+ steps:
18
+ # A. Scarica il codice dalla tua repository
19
+ - name: Checkout code
20
+ uses: actions/checkout@v3 # serve proprio a dire al robot di GitHub: "Scarica la versione del codice contenuta in questo specifico commit che ha appena fatto scattare il trigger".
21
+ # Dobbiamo testare sui nuovi file che stiamo pushando, per questo ci assicuriamo di farlo con i nuovi file
22
+
23
+ # B. Installa Python 3.9 (lo stesso del Dockerfile)
24
+ - name: Set up Python 3.9
25
+ uses: actions/setup-python@v4
26
+ with:
27
+ python-version: "3.9"
28
+ cache: 'pip' # <--- FONDAMENTALE: Cacha le librerie (così non riscarica le dipendenze ogni volta)
29
+
30
+ # C. Installa le librerie
31
+ - name: Install dependencies
32
+ run: |
33
+ python -m pip install --upgrade pip
34
+ pip install -r requirements.txt
35
+
36
+ # D. Lancia Pytest
37
+ - name: Run Tests
38
+ run: |
39
+ python -m pytest
40
+
41
+ # JOB 2: Verifica della Build Docker
42
+ # Questo job parte SOLO se "run_tests" ha successo (needs: run_tests)
43
+ build_docker:
44
+ needs: run_tests
45
+ runs-on: ubuntu-latest
46
+
47
+ steps:
48
+ - name: Checkout code
49
+ uses: actions/checkout@v3
50
+
51
+ - name: Build Docker Image
52
+ # Prova a costruire l'immagine. Se il Dockerfile è rotto, questo step fallisce.
53
+ # E' solo un test, dopo che abbiamo fatto girare pytest su test_api.py, ora testiamo che il dockerfile funzioni creando l'immmagine che poi distruggiamo
54
+ run: docker build -t reputation-monitor:test .
55
+
56
+ """
57
+ I file vengono salvati nella repository PRIMA che il test parta. È il fatto che tu abbia "pushato" i file che sveglia il robot e gli fa iniziare il lavoro.
58
+
59
+ Ecco la sequenza temporale esatta:
60
+
61
+ ⏳ La Timeline Reale
62
+ Tu fai git push: I tuoi file vengono caricati su GitHub. In questo istante, il codice nella repository è già aggiornato (anche se fosse rotto).
63
+
64
+ GitHub vede il cambiamento: "Ehi, è arrivato nuovo codice! Devo lanciare la pipeline".
65
+
66
+ Parte la CI (Test & Build): GitHub scarica quel codice appena caricato ed esegue i test e la build Docker.
67
+
68
+ Esito:
69
+
70
+ 🟢 Se passa: Accanto al tuo commit compare una spunta verde. Tutto bene.
71
+
72
+ 🔴 Se fallisce: Accanto al tuo commit compare una croce rossa. MA i file restano lì. GitHub non cancella il tuo codice se il test fallisce; ti avvisa solo che è "bacato".
73
+
74
+ Se il codice rotto viene caricato comunque, a che serve il test?
75
+
76
+ Professionalmente si crea un ramo diverso della repository, un nuovo branch che non va in produzione, e si pusha li facendo partire CI/CD. POi se tutto ok viene anche aggiornato il main che è sacro
77
+ """
Dockerfile CHANGED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Usiamo un'immagine base ufficiale di Python (leggera e sicura)
2
+ FROM python:3.9-slim
3
+
4
+ # 2. Impostiamo la cartella di lavoro dentro il container
5
+ WORKDIR /code
6
+
7
+ # 3. Ottimizzazione della Cache:
8
+ # Copiamo PRIMA solo i requirements. Questo permette a Docker di non
9
+ # riscaricare tutte le librerie se cambi solo il codice ma non le dipendenze.
10
+ COPY ./requirements.txt /code/requirements.txt
11
+
12
+ # 4. Installiamo le dipendenze
13
+ # --no-cache-dir serve a mantenere l'immagine leggera
14
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
15
+
16
+ # 5. Copiamo tutto il resto del codice dentro il container
17
+ COPY ./app /code/app
18
+
19
+ # 6. (Opzionale ma consigliato) Scarichiamo il modello durante la build
20
+ # In MLOps avanzato si fa per evitare che il container debba scaricare
21
+ # 500MB ogni volta che si avvia.
22
+ # Creiamo un piccolo script inline per fare il "pre-load"
23
+ RUN python -c "from transformers import AutoTokenizer, AutoModelForSequenceClassification; \
24
+ name='cardiffnlp/twitter-roberta-base-sentiment-latest'; \
25
+ AutoTokenizer.from_pretrained(name); \
26
+ AutoModelForSequenceClassification.from_pretrained(name)"
27
+
28
+ # 7. Esponiamo la porta 8000
29
+ EXPOSE 8000
30
+
31
+ # 8. Il comando che parte quando avvii il container
32
+ CMD ["uvicorn", "app.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
app/__init__.py CHANGED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SPIEGAZIONE DI QUESTO FILE
3
+
4
+ Immagina le cartelle del tuo computer come delle semplici scatole. Per Python, una cartella è solo una scatola "muta": non sa che dentro c'è del codice collegato che può essere usato altrove.
5
+
6
+ Il file __init__.py serve a trasformare quella scatola in un Pacchetto (Package).
7
+
8
+ 1. La metafora della "Bandiera" 🚩
9
+ Pensa al file __init__.py come a una bandiera piantata sopra la cartella che dice a Python:
10
+
11
+ "Ehi! Questa non è una cartella qualsiasi piena di file a caso. Questa è una libreria di codice Python! Puoi entrare qui e importare le funzioni che trovi."
12
+
13
+ 2. Cosa fa tecnicamente?
14
+ Senza __init__.py: Se scrivi from app.api import main, Python potrebbe dirti "Non trovo app", perché la tratta come una semplice directory di file.
15
+
16
+ Con __init__.py: Python riconosce app come un oggetto importabile e ti permette di navigare al suo interno con il punto (.).
17
+
18
+ 3. Perché ti serviva per i test?
19
+ Quando hai lanciato pytest, lui doveva collegare due mondi separati: la cartella tests e la cartella app. Mettendo __init__.py, hai detto a Python che tutto il tuo progetto è un insieme di moduli collegati, permettendo al file di test di "vedere" e importare il codice della tua applicazione principale.
20
+
21
+ Nota: Il file può essere (e spesso è) completamente vuoto. La sua sola presenza è sufficiente a fare la magia.
22
+
23
+ """
app/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/app/__pycache__/__init__.cpython-312.pyc and b/app/__pycache__/__init__.cpython-312.pyc differ
 
app/api/__init__.py CHANGED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SPIEGAZIONE DI QUESTO FILE
3
+
4
+ Immagina le cartelle del tuo computer come delle semplici scatole. Per Python, una cartella è solo una scatola "muta": non sa che dentro c'è del codice collegato che può essere usato altrove.
5
+
6
+ Il file __init__.py serve a trasformare quella scatola in un Pacchetto (Package).
7
+
8
+ 1. La metafora della "Bandiera" 🚩
9
+ Pensa al file __init__.py come a una bandiera piantata sopra la cartella che dice a Python:
10
+
11
+ "Ehi! Questa non è una cartella qualsiasi piena di file a caso. Questa è una libreria di codice Python! Puoi entrare qui e importare le funzioni che trovi."
12
+
13
+ 2. Cosa fa tecnicamente?
14
+ Senza __init__.py: Se scrivi from app.api import main, Python potrebbe dirti "Non trovo app", perché la tratta come una semplice directory di file.
15
+
16
+ Con __init__.py: Python riconosce app come un oggetto importabile e ti permette di navigare al suo interno con il punto (.).
17
+
18
+ 3. Perché ti serviva per i test?
19
+ Quando hai lanciato pytest, lui doveva collegare due mondi separati: la cartella tests e la cartella app. Mettendo __init__.py, hai detto a Python che tutto il tuo progetto è un insieme di moduli collegati, permettendo al file di test di "vedere" e importare il codice della tua applicazione principale.
20
+
21
+ Nota: Il file può essere (e spesso è) completamente vuoto. La sua sola presenza è sufficiente a fare la magia.
22
+
23
+ """
app/api/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/app/api/__pycache__/__init__.cpython-312.pyc and b/app/api/__pycache__/__init__.cpython-312.pyc differ
 
app/api/__pycache__/main.cpython-312.pyc CHANGED
Binary files a/app/api/__pycache__/main.cpython-312.pyc and b/app/api/__pycache__/main.cpython-312.pyc differ
 
app/api/main.py CHANGED
@@ -29,7 +29,12 @@ def home():
29
  # Serve a capire se il container è vivo
30
  @app.get("/health")
31
  def health_check():
32
- return {"status": "ok", "message": "Service is running"}
 
 
 
 
 
33
 
34
  # 5. Endpoint di Previsione (Dummy per ora)
35
  @app.post("/predict", response_model=SentimentResponse)
 
29
  # Serve a capire se il container è vivo
30
  @app.get("/health")
31
  def health_check():
32
+ # VERIFICA: Controlliamo se il modello è stato caricato in memoria
33
+ if model_instance.model is not None:
34
+ return {"status": "ok", "model_loaded": True}
35
+ else:
36
+ # Se il modello non c'è, restituiamo errore 503 (Service Unavailable)
37
+ raise HTTPException(status_code=503, detail="Model not loaded yet")
38
 
39
  # 5. Endpoint di Previsione (Dummy per ora)
40
  @app.post("/predict", response_model=SentimentResponse)
app/model/__init__.py CHANGED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SPIEGAZIONE DI QUESTO FILE
3
+
4
+ Immagina le cartelle del tuo computer come delle semplici scatole. Per Python, una cartella è solo una scatola "muta": non sa che dentro c'è del codice collegato che può essere usato altrove.
5
+
6
+ Il file __init__.py serve a trasformare quella scatola in un Pacchetto (Package).
7
+
8
+ 1. La metafora della "Bandiera" 🚩
9
+ Pensa al file __init__.py come a una bandiera piantata sopra la cartella che dice a Python:
10
+
11
+ "Ehi! Questa non è una cartella qualsiasi piena di file a caso. Questa è una libreria di codice Python! Puoi entrare qui e importare le funzioni che trovi."
12
+
13
+ 2. Cosa fa tecnicamente?
14
+ Senza __init__.py: Se scrivi from app.api import main, Python potrebbe dirti "Non trovo app", perché la tratta come una semplice directory di file.
15
+
16
+ Con __init__.py: Python riconosce app come un oggetto importabile e ti permette di navigare al suo interno con il punto (.).
17
+
18
+ 3. Perché ti serviva per i test?
19
+ Quando hai lanciato pytest, lui doveva collegare due mondi separati: la cartella tests e la cartella app. Mettendo __init__.py, hai detto a Python che tutto il tuo progetto è un insieme di moduli collegati, permettendo al file di test di "vedere" e importare il codice della tua applicazione principale.
20
+
21
+ Nota: Il file può essere (e spesso è) completamente vuoto. La sua sola presenza è sufficiente a fare la magia.
22
+
23
+ """
app/model/__pycache__/__init__.cpython-312.pyc CHANGED
Binary files a/app/model/__pycache__/__init__.cpython-312.pyc and b/app/model/__pycache__/__init__.cpython-312.pyc differ
 
requirements.txt CHANGED
@@ -16,4 +16,4 @@ numpy
16
 
17
  # --- Testing ---
18
  pytest
19
- httpx
 
16
 
17
  # --- Testing ---
18
  pytest
19
+ httpx==0.27.0
tests/__init__.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SPIEGAZIONE DI QUESTO FILE
3
+
4
+ Immagina le cartelle del tuo computer come delle semplici scatole. Per Python, una cartella è solo una scatola "muta": non sa che dentro c'è del codice collegato che può essere usato altrove.
5
+
6
+ Il file __init__.py serve a trasformare quella scatola in un Pacchetto (Package).
7
+
8
+ 1. La metafora della "Bandiera" 🚩
9
+ Pensa al file __init__.py come a una bandiera piantata sopra la cartella che dice a Python:
10
+
11
+ "Ehi! Questa non è una cartella qualsiasi piena di file a caso. Questa è una libreria di codice Python! Puoi entrare qui e importare le funzioni che trovi."
12
+
13
+ 2. Cosa fa tecnicamente?
14
+ Senza __init__.py: Se scrivi from app.api import main, Python potrebbe dirti "Non trovo app", perché la tratta come una semplice directory di file.
15
+
16
+ Con __init__.py: Python riconosce app come un oggetto importabile e ti permette di navigare al suo interno con il punto (.).
17
+
18
+ 3. Perché ti serviva per i test?
19
+ Quando hai lanciato pytest, lui doveva collegare due mondi separati: la cartella tests e la cartella app. Mettendo __init__.py, hai detto a Python che tutto il tuo progetto è un insieme di moduli collegati, permettendo al file di test di "vedere" e importare il codice della tua applicazione principale.
20
+
21
+ Nota: Il file può essere (e spesso è) completamente vuoto. La sua sola presenza è sufficiente a fare la magia.
22
+
23
+ """
tests/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (1.52 kB). View file
 
tests/__pycache__/test_api.cpython-312-pytest-9.0.2.pyc ADDED
Binary file (5.99 kB). View file
 
tests/test_api.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi.testclient import TestClient
2
+ from app.api.main import app
3
+
4
+ # Creiamo un client di test (simula il browser)
5
+ client = TestClient(app)
6
+
7
+ def test_health_check():
8
+ """Verifica che l'endpoint /health risponda status 200"""
9
+ response = client.get("/health")
10
+ assert response.status_code == 200
11
+ assert response.json() == {"status": "ok", "model_loaded": True}
12
+
13
+ def test_prediction_positive():
14
+ """Verifica che il modello riconosca un sentimento positivo"""
15
+ payload = {"text": "I love this service, it is amazing!"}
16
+ response = client.post("/predict", json=payload)
17
+
18
+ assert response.status_code == 200
19
+ json_data = response.json()
20
+ assert "sentiment" in json_data
21
+ assert "confidence" in json_data
22
+ # Ci aspettiamo che sia positivo (o almeno non negativo)
23
+ assert json_data["sentiment"] == "positive"
24
+
25
+ def test_prediction_negative():
26
+ """Verifica che il modello riconosca un sentimento negativo"""
27
+ payload = {"text": "This is terrible, I hate it."}
28
+ response = client.post("/predict", json=payload)
29
+
30
+ assert response.status_code == 200
31
+ assert response.json()["sentiment"] == "negative"
32
+
33
+
34
+ """
35
+ scrivi sul terminale: pytest Serve per lanciare il test
36
+ Oppure: pytest -v per vedere tutti i dettagli
37
+
38
+ P.S: pytest agisce come un detective, quando lancio il comando scanziona la cartella corrente
39
+ e cerca file che iniziano con "test_" o finiscono con "_test.py", una volta entrato nel file lancia solo le funzioni che iniziano con "test_"
40
+ """