Skip to content

Commit 729dfa9

Browse files
committed
module 19 solution
1 parent 45cb968 commit 729dfa9

6 files changed

Lines changed: 245 additions & 0 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
name: CI/CD Pipeline
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
env:
8+
ECR_REPOSITORY: beans-api
9+
TASK_DEFINITION: beans-api
10+
CONTAINER_NAME: beans-api
11+
ECS_SERVICE: beans-api-service
12+
ECS_CLUSTER: beans-api-cluster
13+
14+
jobs:
15+
lint:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: "3.12"
22+
- run: pip install ruff
23+
- run: ruff check main.py quality_check.py
24+
25+
quality-check:
26+
runs-on: ubuntu-latest
27+
needs: lint
28+
steps:
29+
- uses: actions/checkout@v4
30+
- uses: actions/setup-python@v5
31+
with:
32+
python-version: "3.12"
33+
- uses: actions/cache@v4
34+
with:
35+
path: ~/.cache/pip
36+
key: pip-deepchecks-${{ hashFiles('requirements-quality.txt') }}
37+
- uses: actions/cache@v4
38+
with:
39+
path: ~/.cache/huggingface
40+
key: hf-beans-vit
41+
- run: pip install -r requirements-quality.txt
42+
- name: Run Deepchecks quality gate
43+
env:
44+
TOKENIZERS_PARALLELISM: "false"
45+
run: python quality_check.py
46+
47+
build:
48+
runs-on: ubuntu-latest
49+
needs: [lint, quality-check]
50+
steps:
51+
- uses: actions/checkout@v4
52+
- name: Configure AWS credentials
53+
uses: aws-actions/configure-aws-credentials@v4
54+
with:
55+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
56+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
57+
aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }}
58+
aws-region: ${{ secrets.AWS_REGION }}
59+
- name: Login to ECR
60+
uses: aws-actions/amazon-ecr-login@v2
61+
- name: Build and push image
62+
env:
63+
ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }}
64+
run: |
65+
IMAGE_TAG="${GITHUB_SHA:0:8}-$(date +%s)"
66+
IMAGE_URI="$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
67+
docker build -t $IMAGE_URI .
68+
docker push $IMAGE_URI
69+
docker tag $IMAGE_URI $ECR_REGISTRY/$ECR_REPOSITORY:latest
70+
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
71+
72+
deploy:
73+
runs-on: ubuntu-latest
74+
needs: build
75+
steps:
76+
- name: Configure AWS credentials
77+
uses: aws-actions/configure-aws-credentials@v4
78+
with:
79+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
80+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
81+
aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }}
82+
aws-region: ${{ secrets.AWS_REGION }}
83+
- name: Download current task definition
84+
run: |
85+
aws ecs describe-task-definition \
86+
--task-definition ${{ env.TASK_DEFINITION }} \
87+
--query taskDefinition > task-definition.json
88+
- name: Render ECS task definition
89+
id: task-def
90+
uses: aws-actions/amazon-ecs-render-task-definition@v1
91+
with:
92+
task-definition: task-definition.json
93+
container-name: ${{ env.CONTAINER_NAME }}
94+
image: ${{ secrets.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:latest
95+
- name: Deploy to ECS
96+
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
97+
with:
98+
task-definition: ${{ steps.task-def.outputs.task-definition }}
99+
service: ${{ env.ECS_SERVICE }}
100+
cluster: ${{ env.ECS_CLUSTER }}
101+
wait-for-service-stability: true
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM python:3.12-slim
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
RUN pip install --no-cache-dir -r requirements.txt
7+
RUN python -m spacy download en_core_web_sm
8+
9+
COPY main.py .
10+
11+
EXPOSE 8000
12+
13+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

module-19-ci-cd/solution/main.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import spacy
2+
from fastapi import FastAPI, HTTPException
3+
from pydantic import BaseModel
4+
5+
app = FastAPI()
6+
7+
nlp = spacy.load("en_core_web_sm")
8+
9+
10+
class TextRequest(BaseModel):
11+
text: str
12+
13+
14+
@app.post("/predict")
15+
async def predict(request: TextRequest):
16+
if not request.text.strip():
17+
raise HTTPException(status_code=400, detail="Text cannot be empty")
18+
19+
doc = nlp(request.text)
20+
21+
return {
22+
"entities": [
23+
{"word": ent.text, "entity_group": ent.label_, "start": ent.start_char, "end": ent.end_char}
24+
for ent in doc.ents
25+
]
26+
}
27+
28+
29+
@app.get("/health")
30+
def health():
31+
return {"status": "healthy"}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""
2+
Deepchecks quality check for the beans ViT model.
3+
"""
4+
5+
import sys
6+
7+
import numpy as np
8+
from datasets import load_dataset
9+
from deepchecks.core import CheckFailure
10+
from deepchecks.vision import VisionData
11+
from deepchecks.vision.suites import train_test_validation
12+
from deepchecks.vision.vision_data import BatchOutputFormat
13+
from torch.utils.data import DataLoader
14+
from torch.utils.data import Dataset as TorchDataset
15+
from transformers import pipeline
16+
17+
MODEL_NAME = "nateraw/vit-base-beans"
18+
19+
20+
class BeansDataset(TorchDataset):
21+
def __init__(self, hf_dataset):
22+
self.dataset = hf_dataset
23+
24+
def __len__(self):
25+
return len(self.dataset)
26+
27+
def __getitem__(self, idx):
28+
return self.dataset[idx]
29+
30+
31+
def make_collate_fn(pipe, label_names):
32+
def collate_fn(examples):
33+
images_pil = [ex["image"].convert("RGB") for ex in examples]
34+
labels = [ex["labels"] for ex in examples]
35+
predictions_raw = pipe(images_pil)
36+
proba = []
37+
for preds in predictions_raw:
38+
scores = {p["label"]: p["score"] for p in preds}
39+
proba.append([scores.get(lbl, 0.0) for lbl in label_names])
40+
images_np = [np.array(img) for img in images_pil]
41+
return BatchOutputFormat(images=images_np, labels=labels, predictions=proba)
42+
return collate_fn
43+
44+
45+
def build_vision_data(hf_split, pipe, label_names):
46+
dataset = BeansDataset(load_dataset("beans", split=hf_split))
47+
label_map = {i: lbl for i, lbl in enumerate(label_names)}
48+
loader = DataLoader(
49+
dataset,
50+
batch_size=8,
51+
collate_fn=make_collate_fn(pipe, label_names),
52+
)
53+
return VisionData(batch_loader=loader, task_type="classification", label_map=label_map)
54+
55+
56+
def main():
57+
print(f"Loading model: {MODEL_NAME}...")
58+
pipe = pipeline("image-classification", model=MODEL_NAME)
59+
60+
dataset = load_dataset("beans", split="validation")
61+
label_names = dataset.features["labels"].names
62+
63+
print("Building VisionData...")
64+
train_data = build_vision_data("train", pipe, label_names)
65+
test_data = build_vision_data("test", pipe, label_names)
66+
67+
print("Running Deepchecks suite...")
68+
suite = train_test_validation()
69+
result = suite.run(train_data, test_data, max_samples=500)
70+
71+
print(f"{len(result.results)} checks executed.")
72+
73+
# As an example, the model quality check will fail if label drift score exceeds 0.5.
74+
for check_result in result.results:
75+
if isinstance(check_result, CheckFailure):
76+
continue
77+
if check_result.check.name() == "Label Drift":
78+
drift_score = check_result.value["Samples Per Class"]["Drift score"]
79+
if drift_score > 0.5:
80+
print(f" [FAIL] Label Drift: score {drift_score:.3f} exceeds threshold 0.5")
81+
sys.exit(1)
82+
else:
83+
print(f" [PASS] Label Drift: score {drift_score:.3f} ≤ 0.5")
84+
break
85+
86+
print("\nAll checks passed.")
87+
88+
89+
if __name__ == "__main__":
90+
main()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
deepchecks>=0.18.0
2+
transformers>=4.44.0
3+
torch>=2.0.0
4+
datasets>=2.18.0
5+
numpy>=1.24.0
6+
anywidget
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fastapi==0.110.0
2+
uvicorn==0.27.1
3+
spacy>=3.7.0
4+
python-multipart==0.0.9

0 commit comments

Comments
 (0)