Skip to content

Commit f9fa280

Browse files
committed
add CI/CD pipeline
1 parent 518211f commit f9fa280

5 files changed

Lines changed: 150 additions & 0 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: CI/CD Pipeline
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
build-and-push:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v3
15+
16+
- name: Login to Docker Hub
17+
uses: docker/login-action@v3
18+
with:
19+
username: ${{ secrets.DOCKERHUB_USERNAME }}
20+
password: ${{ secrets.DOCKERHUB_TOKEN }}
21+
22+
- name: Build and push Docker image
23+
uses: docker/build-push-action@v5
24+
with:
25+
context: .
26+
push: true
27+
tags: saaddot/fraud-detection-api:latest

Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /api
4+
5+
COPY requirements.txt .
6+
RUN pip install --no-cache-dir -r requirements.txt
7+
8+
COPY . .
9+
10+
EXPOSE 8000
11+
12+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from pathlib import Path
2+
3+
# ------------------------
4+
# Project paths
5+
# ------------------------
6+
BASE_DIR = Path(__file__).resolve().parent.parent
7+
8+
MODEL_PATH = BASE_DIR / "models" / "xgboost.pkl"
9+
SCALER_PATH = BASE_DIR / "artifacts" / "standard_scaler.pkl"
10+
11+
# ------------------------
12+
# Model metadata
13+
# ------------------------
14+
MODEL_NAME = "xgboost_fraud_detector"
15+
MODEL_VERSION = "1.0.0"
16+
17+
# ------------------------
18+
# Inference configuration
19+
# ------------------------
20+
THRESHOLD = 0.5
21+
22+
FEATURE_ORDER = [
23+
"Time",
24+
"V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9",
25+
"V10", "V11", "V12", "V13", "V14", "V15", "V16", "V17", "V18", "V19",
26+
"V20", "V21", "V22", "V23", "V24", "V25", "V26", "V27", "V28",
27+
"Amount"
28+
]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import joblib # type: ignore
2+
import pandas as pd # pyright: ignore[reportMissingModuleSource]
3+
from pathlib import Path
4+
from .config import (
5+
MODEL_PATH,
6+
SCALER_PATH,
7+
THRESHOLD,
8+
FEATURE_ORDER
9+
)
10+
11+
model = joblib.load(MODEL_PATH)
12+
scaler = joblib.load(SCALER_PATH)
13+
14+
def predict_fraud(features: dict) -> dict:
15+
data = pd.DataFrame([features])
16+
17+
# 2. Scale ONLY Time and Amount
18+
data[["Time", "Amount"]] = scaler.transform(
19+
data[["Time", "Amount"]]
20+
)
21+
22+
# Reorder columns to match training schema
23+
data = data[FEATURE_ORDER]
24+
25+
probability = model.predict_proba(data)[0][1]
26+
27+
return {
28+
"fraud_probability": float(probability),
29+
"threshold": THRESHOLD,
30+
"is_fraud": bool(probability >= THRESHOLD)
31+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from fastapi import FastAPI, HTTPException
2+
from .schemas import Transaction
3+
from .inference import predict_fraud
4+
5+
from .config import (
6+
MODEL_NAME,
7+
MODEL_VERSION,
8+
THRESHOLD,
9+
FEATURE_ORDER
10+
)
11+
12+
app = FastAPI(
13+
title="Fraud Detection API",
14+
version="1.0.0"
15+
)
16+
17+
@app.get("/health")
18+
def health_check():
19+
return {"status": "ok"}
20+
21+
@app.post("/predict")
22+
def predict(transaction: Transaction):
23+
try:
24+
return predict_fraud(transaction.model_dump())
25+
26+
except KeyError as e:
27+
raise HTTPException(
28+
status_code=400,
29+
detail=f"Missing required feature: {str(e)}"
30+
)
31+
32+
except ValueError as e:
33+
raise HTTPException(
34+
status_code=400,
35+
detail=str(e)
36+
)
37+
38+
except Exception:
39+
raise HTTPException(
40+
status_code=500,
41+
detail="Internal model inference error"
42+
)
43+
44+
@app.get("/model-info")
45+
def model_info():
46+
return {
47+
"model_name": MODEL_NAME,
48+
"model_version": MODEL_VERSION,
49+
"threshold": THRESHOLD,
50+
"num_features": len(FEATURE_ORDER)
51+
}
52+

0 commit comments

Comments
 (0)