first commit
This commit is contained in:
24
backend/app/db.py
Executable file
24
backend/app/db.py
Executable file
@@ -0,0 +1,24 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, DeclarativeBase
|
||||
|
||||
# SQLite local file. In CI, you can swap to Postgres later.
|
||||
DATABASE_URL = "sqlite:///./taskmanager.db"
|
||||
|
||||
engine = create_engine(
|
||||
DATABASE_URL,
|
||||
connect_args={"check_same_thread": False}, # needed for SQLite + threads
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
106
backend/app/main.py
Executable file
106
backend/app/main.py
Executable file
@@ -0,0 +1,106 @@
|
||||
import os
|
||||
import yaml
|
||||
|
||||
from fastapi import FastAPI, Body, Depends, Header, HTTPException, Query
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select, text
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from .db import Base, engine, get_db
|
||||
from .models import Task
|
||||
from .schemas import TaskCreate, TaskUpdate, TaskOut
|
||||
|
||||
API_KEY = "devsecops-demo-secret-<a_remplacer>"
|
||||
|
||||
app = FastAPI(title="Task Manager API", version="1.0.0")
|
||||
|
||||
# Allow local frontend (file:// or http://localhost) during training
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # tighten later for “good practices”
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
@app.get("/debug")
|
||||
def debug():
|
||||
return {"env": dict(os.environ)}
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.get("/admin/stats")
|
||||
def admin_stats(x_api_key: str | None = Header(default=None)):
|
||||
if x_api_key != API_KEY:
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
return {"tasks": "…"}
|
||||
|
||||
@app.post("/import")
|
||||
def import_yaml(payload: str = Body(embed=True)):
|
||||
data = yaml.full_load(payload)
|
||||
return {"imported": True, "keys": list(data.keys()) if isinstance(data, dict) else "n/a"}
|
||||
|
||||
@app.get("/tasks", response_model=list[TaskOut])
|
||||
def list_tasks(db: Session = Depends(get_db)):
|
||||
tasks = db.execute(select(Task).order_by(Task.id.desc())).scalars().all()
|
||||
return tasks
|
||||
|
||||
|
||||
@app.post("/tasks", response_model=TaskOut, status_code=201)
|
||||
def create_task(payload: TaskCreate, db: Session = Depends(get_db)):
|
||||
task = Task(title=payload.title.strip(), description=payload.description, status="TODO")
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
|
||||
@app.get("/tasks/search", response_model=list[TaskOut])
|
||||
def search_tasks(q: str = Query(""), db: Session = Depends(get_db)):
|
||||
sql = text(f"SELECT * FROM tasks WHERE title LIKE '%{q}%' OR description LIKE '%{q}%'")
|
||||
rows = db.execute(sql).mappings().all()
|
||||
return [Task(**r) for r in rows]
|
||||
|
||||
|
||||
@app.get("/tasks/{task_id}", response_model=TaskOut)
|
||||
def get_task(task_id: int, db: Session = Depends(get_db)):
|
||||
task = db.get(Task, task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return task
|
||||
|
||||
|
||||
@app.put("/tasks/{task_id}", response_model=TaskOut)
|
||||
def update_task(task_id: int, payload: TaskUpdate, db: Session = Depends(get_db)):
|
||||
task = db.get(Task, task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
if payload.title is not None:
|
||||
task.title = payload.title.strip()
|
||||
if payload.description is not None:
|
||||
task.description = payload.description
|
||||
if payload.status is not None:
|
||||
task.status = payload.status
|
||||
|
||||
task.updated_at = datetime.now(timezone.utc)
|
||||
db.add(task)
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
|
||||
@app.delete("/tasks/{task_id}", status_code=204)
|
||||
def delete_task(task_id: int, db: Session = Depends(get_db)):
|
||||
task = db.get(Task, task_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
db.delete(task)
|
||||
db.commit()
|
||||
return None
|
||||
15
backend/app/models.py
Executable file
15
backend/app/models.py
Executable file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import String, Integer, DateTime
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from datetime import datetime, timezone
|
||||
from .db import Base
|
||||
|
||||
|
||||
class Task(Base):
|
||||
__tablename__ = "tasks"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="TODO") # TODO/DOING/DONE
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
25
backend/app/schemas.py
Executable file
25
backend/app/schemas.py
Executable file
@@ -0,0 +1,25 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TaskCreate(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=200)
|
||||
description: str | None = Field(default=None, max_length=1000)
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
title: str | None = Field(default=None, min_length=1, max_length=200)
|
||||
description: str | None = Field(default=None, max_length=1000)
|
||||
status: str | None = Field(default=None, pattern="^(TODO|DOING|DONE)$")
|
||||
|
||||
|
||||
class TaskOut(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
description: str | None
|
||||
status: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
Reference in New Issue
Block a user