The M8 evaluation proved the model’s accuracy; M9 turns that insight into a usable product .
By exposing inference through both a command‑line interface (CLI) and a FastAPI micro‑service we:
Unify Workflow – Analysts can batch‑score tweets locally, while engineers hit an HTTP endpoint.
Decouple Deployment – A lightweight Python wheel or Docker image can be shipped independently of notebooks.
Show Engineering Rigor – Recruiters see production‑grade habits: argument parsing, type hints, and pydantic validation.
1. Imports & paths
Code
from __future__ import annotations
import sys
import threading
import time
from pathlib import Path
from textwrap import dedent
import requests
import uvicorn
# repo roots
ROOT_DIR = Path.cwd().resolve().parent
MODELS_DIR = ROOT_DIR / "models"
SRC_DIR = ROOT_DIR / "src"
SRC_DIR.mkdir(parents= True , exist_ok= True )
# make `src/` importable for local modules
if str (SRC_DIR) not in sys.path:
sys.path.insert(0 , str (SRC_DIR))
MODEL_PATH = MODELS_DIR / "logreg_tfidf.joblib"
if not MODEL_PATH.exists():
raise FileNotFoundError ("Trained model missing; run M8 first." )
2. Writing The CLI Utility
The goal is a single‑file script (src/predict.py) that:
Loads the persisted TF‑IDF + LogReg pipeline once on startup (≈ 1 MB, < 100 ms).
Accepts free‑form text as positional arguments.
Prints the predicted label (negative, neutral, positive) with zero boilerplate.
Key design choices
argparse (standard library)
No external dependency; instantly familiar to reviewers.
Eager model load at module import
Keeps per‑call latency to near‑zero.
Pure function predict(text)
Simplifies reuse inside other Python apps or tests.
Code
# Write CLI script
pred_path = (SRC_DIR / "predict.py" ).as_posix()
Code
%% writefile {pred_path}
"""CLI inference utility for the airline‑sentiment model."""
from __future__ import annotations
import argparse
from pathlib import Path
import joblib
MODEL_PATH = Path(__file__ ).resolve().parents[1 ] / "models" / "logreg_tfidf.joblib"
_PIPE = joblib.load(MODEL_PATH) # eager load; model ≈1 MB
def predict(text: str ) -> str :
"""Return the sentiment class for a single tweet."""
return _PIPE.predict([text])[0 ]
def main() -> None : # pragma: no cover
parser = argparse.ArgumentParser(description= "Predict tweet sentiment." )
parser.add_argument("text" , nargs= "+" , help = "Text to classify." )
args = parser.parse_args()
print (predict(" " .join(args.text)))
if __name__ == "__main__" :
main()
Overwriting C:/Projects/twitter-airline-analysis/src/predict.py
3. Building The FastAPI Micro‑Service
FastAPI offers automatic OpenAPI docs and pydantic validation:
Schema Safety – InferenceRequest and InferenceResponse enforce a stable contract.
Async‑Ready – The service can scale under ASGI servers like Uvicorn or Gunicorn with Uvicorn workers.
Minimal Footprint – The entire file is ~40 lines, yet covers validation, error handling, and type hints.
Note: Import latency is negligible; loading the model adds ~50 ms cold‑start on a laptop, acceptable for most serverless platforms.
Code
%% writefile {SRC_DIR / "app.py" }
"""FastAPI wrapper exposing /predict endpoint."""
from __future__ import annotations
from pathlib import Path
from typing import Literal
import joblib
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
MODEL_PATH = Path(__file__ ).resolve().parents[1 ] / "models" / "logreg_tfidf.joblib"
PIPE = joblib.load(MODEL_PATH)
class InferenceRequest(BaseModel):
text: str = Field(..., example= "My flight was delayed 3 hours" )
class InferenceResponse(BaseModel):
label: Literal["negative" , "neutral" , "positive" ]
app = FastAPI(
title= "Airline Sentiment Inference API" ,
version= "0.1.0" ,
summary= "Lightweight TF‑IDF + LogReg sentiment classifier" ,
)
@app.post ("/predict" , response_model= InferenceResponse)
def predict(req: InferenceRequest) -> InferenceResponse: # noqa: D401
"""Return the sentiment label for the supplied text."""
if not req.text.strip():
raise HTTPException(status_code= 400 , detail= "Text must be non‑empty." )
label = PIPE.predict([req.text])[0 ]
return InferenceResponse(label= label)
Overwriting C:\Projects\twitter-airline-analysis\src\app.py
4. Smoke‑Testing The CLI
We invoke the script via subprocess.run to ensure:
The module resolves via python -m src.predict.
Prediction executes end‑to‑end without touching notebook globals.
Exit code is 0 and stdout contains a valid class label.
This test guards against path mishaps (e.g., missing models/ folder) before CI.
Code
# Smoke‑test CLI
import subprocess # noqa: D401
example = "Loved the crew but the flight was late."
result = subprocess.run(
["python" , SRC_DIR / "predict.py" , example],
capture_output= True ,
text= True ,
check= True ,
).stdout.strip()
print ("CLI prediction →" , result)
CLI prediction → negative
5. Smoke‑Testing The API
Two complementary strategies were used:
Uvicorn Thread – Spins up the ASGI server in‑process, hits /predict over HTTP, and checks the JSON payload.
FastAPI TestClient (optional alternative) – Runs in‑process without networking, ideal for unit tests.
Both confirm that pydantic validation, routing, and the model itself cooperate seamlessly.
Code
ROOT_DIR = Path.cwd().resolve().parents[0 ]
sys.path.insert(0 , str (ROOT_DIR)) # make `import src.app` resolvable
def _run_app() -> None :
uvicorn.run(
"src.app:app" ,
host= "127.0.0.1" ,
port= 8000 ,
log_level= "warning" ,
reload = False ,
)
thread = threading.Thread(target= _run_app, daemon= True )
thread.start()
time.sleep(2 ) # allow cold‑start
payload = {"text" : "This is the best flight ever!" }
resp = requests.post("http://127.0.0.1:8000/predict" , json= payload, timeout= 5 )
print ("API response →" , resp.json())
API response → {'label': 'positive'}
6. Persisting Usage Snippets
The docs/inference_usage.md helper lowers the barrier for new users:
Copy‑Paste Ready CLI – One‑liner to classify a tweet.
Curl Example – Demonstrates the JSON contract for the REST endpoint.
Keeping quick‑start commands under docs/ surfaces polish and documentation discipline—details hiring managers notice.
Code
DOCS_DIR = ROOT_DIR / "docs"
DOCS_DIR.mkdir(exist_ok= True )
snippet = dedent(
"""
### Inference Usage
```bash
# CLI
python -m src.predict "Flight delayed again :(" # ➜ negative
```
```bash
# API (local dev)
uvicorn src.app:app --host 0.0.0.0 --port 8000
# then:
curl -X POST http://127.0.0.1:8000/predict \\
-H "Content-Type: application/json" \\
-d '{ "text": "Great service!" }'
# ➜ {"label":"positive"}
```
"""
).strip() + " \n "
filepath = DOCS_DIR / "inference_usage.md"
filepath.write_text(snippet, encoding= "utf-8" )
print (f"✓ Wrote { filepath. relative_to(ROOT_DIR)} " )
✓ Wrote docs\inference_usage.md
Next Steps
Dockerise – Add a multi‑stage Dockerfile (python-slim base) for parity across environments.
CI Pipeline – Extend GitHub Actions to build the image, run the TestClient suite, and push to GHCR.
Versioned Releases – Tag v1.0.0 once Docker and docs are green; attach artefacts to the release page.