First version.

This commit is contained in:
Oleksandr Kozachuk
2025-06-04 00:00:44 +02:00
commit 0331bdb9a9
2 changed files with 1050 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
import os
import json
import uuid
from typing import Optional
from datetime import datetime
from fastapi import FastAPI, HTTPException, Path
from pydantic import BaseModel, Field
import argparse
import uvicorn
app = FastAPI(title="Tour Voting API")
# ─── Pydantic MODELS ──────────────────────────────────────────────────────────
class Idea(BaseModel):
id: str
name: str
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
description: str
voters: list[str] = Field(default_factory=list)
class Tour(BaseModel):
id: str
name: str
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
description: str
ideas: list[Idea] = Field(default_factory=list)
createdAt: datetime
class TourCreate(BaseModel):
name: str
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
description: str
class IdeaCreate(BaseModel):
name: str
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
description: str
class Vote(BaseModel):
voterName: str
# ─── GLOBAL CONFIG ────────────────────────────────────────────────────────────
DATA_DIR = "" # will be overridden via command line
def get_tour_filepath(tour_id: str) -> str:
return os.path.join(DATA_DIR, f"{tour_id}.json")
# ─── ENDPOINT: CREATE TOUR ─────────────────────────────────────────────────────
@app.post("/tour/v1/tours", response_model=Tour)
def create_tour(tour: TourCreate):
"""
Create a new tour. Generates its own UUID and timestamp, then writes to {tour_id}.json.
"""
tour_id = str(uuid.uuid4())
created_at = datetime.utcnow()
new_tour = Tour(
id=tour_id,
name=tour.name,
start_date=tour.start_date,
end_date=tour.end_date,
description=tour.description,
ideas=[],
createdAt=created_at,
)
filepath = get_tour_filepath(tour_id)
with open(filepath, "w") as f:
# Using default=str so that datetime objects become ISO strings
json.dump(new_tour.dict(), f, default=str)
return new_tour
# ─── ENDPOINT: GET TOUR ────────────────────────────────────────────────────────
@app.get("/tour/v1/tours/{tour_id}", response_model=Tour)
def get_tour(tour_id: str = Path(...)):
"""
Retrieve an existing tour by ID. Loads {tour_id}.json and parses back into a Tour.
"""
filepath = get_tour_filepath(tour_id)
if not os.path.exists(filepath):
raise HTTPException(status_code=404, detail="Tour not found")
with open(filepath) as f:
data = json.load(f)
if data.get("start_date"):
data["start_date"] = datetime.fromisoformat(data["start_date"])
if data.get("end_date"):
data["end_date"] = datetime.fromisoformat(data["end_date"])
data["createdAt"] = datetime.fromisoformat(data["createdAt"])
# Convert nested idea timestamps as well
for idea in data["ideas"]:
if idea.get("start_time"):
idea["start_time"] = datetime.fromisoformat(idea["start_time"])
if idea.get("end_time"):
idea["end_time"] = datetime.fromisoformat(idea["end_time"])
return Tour(**data)
# ─── ENDPOINT: ADD IDEA ─────────────────────────────────────────────────────────
@app.post("/tour/v1/tours/{tour_id}/ideas", response_model=Idea)
def add_idea(tour_id: str, idea: IdeaCreate):
"""
Add a new idea to the given tour. Generates a new UUID for the idea and appends it.
"""
filepath = get_tour_filepath(tour_id)
if not os.path.exists(filepath):
raise HTTPException(status_code=404, detail="Tour not found")
with open(filepath) as f:
data = json.load(f)
idea_id = str(uuid.uuid4())
new_idea = Idea(
id=idea_id,
name=idea.name,
description=idea.description,
voters=[],
start_time=idea.start_time,
end_time=idea.end_time,
)
data["ideas"].append(new_idea.dict())
with open(filepath, "w") as f:
json.dump(data, f, default=str)
return new_idea
# ─── ENDPOINT: VOTE FOR IDEA ───────────────────────────────────────────────────
@app.post("/tour/v1/tours/{tour_id}/ideas/{idea_id}/vote", response_model=Idea)
def vote_idea(tour_id: str, idea_id: str, vote: Vote):
"""
Cast a vote on a specific idea. Adds voterName if not already present.
"""
filepath = get_tour_filepath(tour_id)
if not os.path.exists(filepath):
raise HTTPException(status_code=404, detail="Tour not found")
with open(filepath) as f:
data = json.load(f)
for idea in data["ideas"]:
if idea["id"] == idea_id:
if vote.voterName not in idea["voters"]:
idea["voters"].append(vote.voterName)
# Persist the change
with open(filepath, "w") as f:
json.dump(data, f, default=str)
return Idea(**idea)
raise HTTPException(status_code=404, detail="Idea not found")
# ─── MAIN: RUN UVIDORN WITH COMMAND-LINE DATA DIR ──────────────────────────────
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Run Tour Voting API server")
parser.add_argument(
"--data-dir",
required=True,
help="Directory in which to store tour JSON files (one file per tour_id)",
)
parser.add_argument(
"--host",
default="0.0.0.0",
help="Host to run the server on",
)
parser.add_argument(
"--port",
type=int,
default=8000,
help="Port to run the server on",
)
args = parser.parse_args()
DATA_DIR = args.data_dir
os.makedirs(DATA_DIR, exist_ok=True)
uvicorn.run(app, host=args.host, port=args.port, reload=False)