Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ log() {
echo -e "\033[0;32m$1\033[0m"
}


run_tests_locally(){
log "====== installing dependencies locally ======"
pip install --upgrade pip
pip install --no-cache-dir -r requirements.txt

log "✅ Running unit tests.... %f"
pytest
}

source ../venv/bin/activate
set -e

NETWORK="dating-app-network"
FAPI_LOCAL_PORT=8000
FAPI_KUBE_PORT=9000
Expand Down
6 changes: 6 additions & 0 deletions helm_charts/my-stack/charts/fastapi/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v2
name: fastapi
description: FastAPI app
type: application
version: 0.1.0
appVersion: "1.0.0"
31 changes: 31 additions & 0 deletions helm_charts/my-stack/charts/fastapi/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: fastapi-deployment
spec:
replicas: 1
selector:
matchLabels:
app: fastapi
template:
metadata:
labels:
app: fastapi
spec:
containers:
- name: fastapi
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: Never
ports:
- containerPort: 8000
env :
- name: DB_PASS
value: "pass123"
- name: DB_USER
value: "aiuser"
- name: DB_NAME
value: "profiledb"
- name: DB_HOST
value: "postgres-0.postgres.default.svc.cluster.local"
- name: DB_PORT
value: "5432"
12 changes: 12 additions & 0 deletions helm_charts/my-stack/charts/fastapi/templates/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: fastapi-service
spec:
type: NodePort
selector:
app: fastapi
ports:
- port: 8000
targetPort: 8000
nodePort: 30080
9 changes: 4 additions & 5 deletions minikube_deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,26 @@
source ./common.sh
RELEASE_TYPE=$1
if [ -z "$RELEASE_TYPE" ]; then
echo "No RELEASE_TYPE provided. Running in default mode."
echo "No RELEASE_TYPE provided. Shallow deploy in progress."
RELEASE_TYPE="default"
fi

set -e

log "🚀 Starting minikube..."
minikube start
run_tests_locally

log "🚀 getting k8ts ready..."
eval "$(minikube docker-env)"
docker build -t $FAPI_IMAGE_NAME .
minikube image load $FAPI_IMAGE_NAME
#minikube image load $FAPI_IMAGE_NAME
if [ "$RELEASE_TYPE" == "ff" ]; then
(
set +e
kubectl delete -f k8s/
kubectl apply -f k8s/
)
else
kubectl rollout restart deployment fastapi
kubectl rollout restart deployment fastapi-deployment
fi

kubectl port-forward service/fastapi-service $FAPI_KUBE_PORT:$FAPI_LOCAL_PORT
19 changes: 18 additions & 1 deletion source/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from fastapi import FastAPI, Response, Depends
from fastapi import FastAPI, Response, Depends, HTTPException
from source.dao.profile_dao import ProfileDao
from source.models.profile import ProfileModel
from source.models.InsertProfile import InsertProfileModel
import logging

app = FastAPI()
Expand Down Expand Up @@ -31,3 +32,19 @@ def read_root():
def get_random_profile(dao: ProfileDao = Depends(get_dao)):
profile = dao.fetch_random_profile()
return profile or {"error": "no profiles found"}


@app.get("/profiles/profile/{profile_id}", response_model=ProfileModel)
def get_random_profile(profile_id: int, dao: ProfileDao = Depends(get_dao)):
profile = dao.get_profile_by_id(profile_id)
if profile is None:
raise HTTPException(status_code=404, detail="Profile not found")
return profile


@app.post("/profiles", status_code=201, response_model=ProfileModel)
def create_profile(profile: InsertProfileModel, dao: ProfileDao = Depends(get_dao)):
profile = dao.insert_profile(profile.name, profile.description)
if profile is None:
raise HTTPException(status_code=404, detail="Profile not found")
return profile
26 changes: 26 additions & 0 deletions source/dao/profile_dao.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import psycopg2
from psycopg2.extras import RealDictCursor
from source.configuration.profile_db_config import ProfileConfig

logger = logging.getLogger(__name__)
Expand All @@ -22,3 +23,28 @@ def fetch_random_profile(self):
if row:
return {"id": row[0], "name": row[1], "description": row[2]}
return None

def get_profile_by_id(self, profile_id: int):
conn = psycopg2.connect(self.db_url)
cur = conn.cursor()
cur.execute("SELECT id, name, description FROM profiles where id = %s", [profile_id])
row = cur.fetchone()
cur.close()
conn.close()
if row:
return {"id": row[0], "name": row[1], "description": row[2]}
return None

def insert_profile(self, name: str, description: str):
conn = psycopg2.connect(self.db_url)
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("INSERT INTO profiles (name, description) VALUES (%s, %s) RETURNING *", (name, description))
inserted_profile = cur.fetchone()
if not inserted_profile:
conn.rollback()
raise Exception("Insert failed: no row returned")
conn.commit()
cur.close()
conn.close()
return inserted_profile

6 changes: 6 additions & 0 deletions source/models/InsertProfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class InsertProfileModel(BaseModel):
name: str
description: str
Empty file added source/models/__init__.py
Empty file.
51 changes: 50 additions & 1 deletion test/test_main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
from fastapi.testclient import TestClient
from source.app import app, get_dao

fake_profile_json = {"id": 42, "name": "Stub", "description": "Fake profile"}
fake_id = 42
fake_name = "stubbed name"
fake_description = "stubbed description"
fake_profile_json = {"id": fake_id, "name": fake_name, "description": fake_description}


class StubProfileDAO:
last_inserted = {}

def fetch_random_profile(self):
return fake_profile_json

def get_profile_by_id(self, profile_id: int):
return fake_profile_json

def insert_profile(self, name: str, description: str):
fake_inserted_profile = {
"id": fake_id,
"name": name,
"description": description
}
self.last_inserted = fake_inserted_profile
return fake_profile_json




def test_root_custom_header():
client = TestClient(app)
Expand All @@ -27,3 +46,33 @@ def test_random_profile():

app.dependency_overrides = {}
return None


def test_profile_by_id():
app.dependency_overrides[get_dao] = lambda: StubProfileDAO()

client = TestClient(app)
response = client.get("/profiles/profile/1")
assert response.status_code == 200
assert response.json() == fake_profile_json

app.dependency_overrides = {}
return None


def test_insert_profile():
stub_dao = StubProfileDAO()
app.dependency_overrides[get_dao] = lambda: stub_dao

client = TestClient(app)
payload = {"name": fake_name, "description": fake_description}

response = client.post('/profiles/', json=payload)
expected_profile = {
"id": fake_id,
"name": fake_name,
"description": fake_description
}
assert response.json() == expected_profile
assert stub_dao.last_inserted == expected_profile
return None