init
This commit is contained in:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.env
|
||||
.venv
|
||||
venv/
|
||||
.git/
|
||||
.gitignore
|
||||
docker-compose.yml
|
||||
README.md
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.env
|
||||
.venv
|
||||
venv/
|
||||
36
Dockerfile
Normal file
36
Dockerfile
Normal 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
39
README.md
Normal 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
125
app/main.py
Normal 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
36
docker-compose.yml
Normal 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
5
requirements.txt
Normal 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
|
||||
Reference in New Issue
Block a user