This commit is contained in:
2026-05-19 10:00:21 +02:00
commit 4ffaaaa96d
7 changed files with 257 additions and 0 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
__pycache__/
*.pyc
*.pyo
.env
.venv
venv/
.git/
.gitignore
docker-compose.yml
README.md

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
__pycache__/
*.pyc
*.pyo
.env
.venv
venv/

36
Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
# ── Stage 1: builder ─────────────────────────────────────────────────────────
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
# Kompajliraj dependencies u wheel fajlove
RUN pip install --upgrade pip && \
pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# ── Stage 2: production ───────────────────────────────────────────────────────
FROM python:3.12-slim
# Ne koristiti root
RUN adduser --disabled-password --gecos "" appuser
WORKDIR /app
# Kopiraj samo wheel fajlove iz buildera
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/* && \
rm -rf /wheels
# Kopiraj aplikaciju
COPY app/ ./app/
# Prebaci na non-root korisnika
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

39
README.md Normal file
View File

@@ -0,0 +1,39 @@
# fastapi-todo-api
Minimalan FastAPI REST API s PostgreSQL bazom — za studentske GitOps vježbe na FPMOZ.
## Endpointi
| Metoda | URL | Opis |
|--------|-----|------|
| GET | `/health` | Status aplikacije i baze |
| GET | `/todos` | Lista svih zapisa |
| GET | `/todos/{id}` | Jedan zapis |
| POST | `/todos` | Kreiraj zapis |
| PUT | `/todos/{id}` | Ažuriraj zapis |
| DELETE | `/todos/{id}` | Obriši zapis |
Swagger dokumentacija: `http://localhost:8000/docs`
## Lokalno pokretanje
```
docker compose up --build
```
## Environment varijable
| Varijabla | Default | Opis |
|-----------|---------|------|
| `DATABASE_HOST` | `localhost` | Hostname baze |
| `DATABASE_PORT` | `5432` | Port baze |
| `POSTGRES_USER` | `fastapiuser` | Korisnik baze |
| `POSTGRES_PASSWORD` | `student123` | Lozinka baze |
| `POSTGRES_DB` | `fastapidb` | Naziv baze |
## Build i push na Docker Hub
```
docker build -t vasic/fastapi-todo-api:1.0 .
docker push vasic/fastapi-todo-api:1.0
```

125
app/main.py Normal file
View File

@@ -0,0 +1,125 @@
from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy import create_engine, Column, Integer, String, Boolean, text
from sqlalchemy.orm import declarative_base, Session, sessionmaker
from pydantic import BaseModel
from typing import Optional
import os
import time
import logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
# ── Database connection ───────────────────────────────────────────────────────
DB_HOST = os.getenv("DATABASE_HOST", "localhost")
DB_PORT = os.getenv("DATABASE_PORT", "5432")
DB_USER = os.getenv("POSTGRES_USER", "fastapiuser")
DB_PASS = os.getenv("POSTGRES_PASSWORD", "student123")
DB_NAME = os.getenv("POSTGRES_DB", "fastapidb")
DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
# Retry logic — baza mozda nije odmah dostupna kad Pod starta
engine = None
for attempt in range(10):
try:
engine = create_engine(DATABASE_URL)
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
log.info(f"Baza dostupna (pokusaj {attempt + 1})")
break
except Exception as e:
log.warning(f"Baza nije dostupna, cekam 3s... (pokusaj {attempt + 1}): {e}")
time.sleep(3)
if engine is None:
raise RuntimeError("Ne mogu se spojiti na bazu nakon 10 pokusaja")
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()
# ── Model ─────────────────────────────────────────────────────────────────────
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
done = Column(Boolean, default=False)
Base.metadata.create_all(bind=engine)
# ── Schemas ───────────────────────────────────────────────────────────────────
class TodoCreate(BaseModel):
title: str
done: Optional[bool] = False
class TodoUpdate(BaseModel):
title: Optional[str] = None
done: Optional[bool] = None
class TodoOut(BaseModel):
id: int
title: str
done: bool
model_config = {"from_attributes": True}
# ── App ───────────────────────────────────────────────────────────────────────
app = FastAPI(title="FastAPI Todo API", version="1.0.0")
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# ── Routes ────────────────────────────────────────────────────────────────────
@app.get("/health")
def health():
try:
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
return {"status": "ok", "database": "connected"}
except Exception as e:
raise HTTPException(status_code=503, detail=f"Baza nedostupna: {e}")
@app.get("/todos", response_model=list[TodoOut])
def get_todos(db: Session = Depends(get_db)):
return db.query(Todo).all()
@app.get("/todos/{todo_id}", response_model=TodoOut)
def get_todo(todo_id: int, db: Session = Depends(get_db)):
todo = db.query(Todo).filter(Todo.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="Todo nije pronaden")
return todo
@app.post("/todos", response_model=TodoOut, status_code=201)
def create_todo(payload: TodoCreate, db: Session = Depends(get_db)):
todo = Todo(title=payload.title, done=payload.done)
db.add(todo)
db.commit()
db.refresh(todo)
return todo
@app.put("/todos/{todo_id}", response_model=TodoOut)
def update_todo(todo_id: int, payload: TodoUpdate, db: Session = Depends(get_db)):
todo = db.query(Todo).filter(Todo.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="Todo nije pronaden")
if payload.title is not None:
todo.title = payload.title
if payload.done is not None:
todo.done = payload.done
db.commit()
db.refresh(todo)
return todo
@app.delete("/todos/{todo_id}")
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
todo = db.query(Todo).filter(Todo.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="Todo nije pronaden")
db.delete(todo)
db.commit()
return {"message": f"Todo {todo_id} obrisan"}

36
docker-compose.yml Normal file
View File

@@ -0,0 +1,36 @@
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: fastapiuser
POSTGRES_PASSWORD: student123
POSTGRES_DB: fastapidb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD", "pg_isready", "-U", "fastapiuser", "-d", "fastapidb"]
interval: 5s
timeout: 5s
retries: 10
api:
build: .
restart: unless-stopped
ports:
- "8000:8000"
environment:
DATABASE_HOST: db
DATABASE_PORT: "5432"
POSTGRES_USER: fastapiuser
POSTGRES_PASSWORD: student123
POSTGRES_DB: fastapidb
depends_on:
db:
condition: service_healthy
volumes:
postgres_data:

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
fastapi==0.111.0
uvicorn==0.30.1
sqlalchemy==2.0.30
psycopg2-binary==2.9.9
pydantic==2.7.1