diff --git a/.env.test b/.env.test index 8cdfcb1..d790c58 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,3 @@ -DATABASE_URI="postgresql://felix@localhost:5432/whisper_api_test" +DATABASE_URI="postgresql://postgres:postgres@localhost:5432/whisper_api_test" ENVIRONMENT="development" API_SECRET="foo" diff --git a/Makefile b/Makefile index 9b2e58f..32ca95e 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ - dev: - uvicorn app.main:app --reload + docker-compose -f dev.docker-compose.yml up --build --remove-orphans fmt: black app diff --git a/README.md b/README.md index f819fd6..b57765a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1 @@ # whisper-api - -### TODO - -- [ ] run alembic migrations before startup -- [ ] dockerize -- [ ] add celery queue diff --git a/app/main.py b/app/main.py index 0434d61..c47d7b4 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,13 @@ -from typing import Dict, List +from typing import Dict, List, Optional +from uuid import UUID -from fastapi import APIRouter, Depends, FastAPI +from fastapi import APIRouter, Depends, FastAPI, HTTPException, Path +from pydantic import AnyHttpUrl, BaseModel +from sqlalchemy.orm import Session +from app.db.base import get_session + +from app.db.dtos import Job, JobStatus, JobType +import app.db.models as models from .security import authenticate_api_key @@ -14,23 +21,47 @@ def api_root() -> Dict: return {} -@api_router.post("/transcripts") -def create_transcript() -> None: - return None +class TranscriptPayload(BaseModel): + url: AnyHttpUrl -@api_router.get("/transcripts") -def get_transcripts() -> List: - return [] +@api_router.post("/transcripts", response_model=Job) +def create_transcript( + payload: TranscriptPayload, session: Session = Depends(get_session) +) -> models.Job: + job = models.Job(url=payload.url, status=JobStatus.Create, type=JobType.Transcript) + session.add(job) + session.flush() + return job -@api_router.get("/transcripts/{id}") -def get_transcript() -> None: - return None +@api_router.get("/transcripts", response_model=List[Job]) +def get_transcripts(session: Session = Depends(get_session)) -> List[models.Job]: + return session.query(models.Job).filter(models.Job.type == JobType.Transcript).all() + + +@api_router.get("/transcripts/{id}", response_model=Job) +def get_transcript( + id: UUID = Path(), session: Session = Depends(get_session) +) -> Optional[Job]: + job = ( + session.query(models.Job) + .filter(models.Job.id == id) + .filter(models.Job.type == JobType.Transcript) + .one_or_none() + ) + if not job: + raise HTTPException(status_code=404) + return job @api_router.delete("/transcripts/{id}") -def delete_transcript() -> None: +def delete_transcript( + id: UUID = Path(), session: Session = Depends(get_session) +) -> None: + session.query(models.Job).filter(models.Job.id == id).filter( + models.Job.type == JobType.Transcript + ).delete() return None diff --git a/app/security.py b/app/security.py index 6194b5d..d45ae3d 100644 --- a/app/security.py +++ b/app/security.py @@ -5,10 +5,10 @@ from fastapi.security import OAuth2PasswordBearer from app.config import settings -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - -def authenticate_api_key(token: str = Depends(oauth2_scheme)) -> None: +def authenticate_api_key( + token: str = Depends(OAuth2PasswordBearer(tokenUrl="token")), +) -> None: if not token: raise HTTPException(status_code=422) # use compare_digest to counter timing attacks. diff --git a/app/start.sh b/app/start.sh new file mode 100755 index 0000000..99ebd80 --- /dev/null +++ b/app/start.sh @@ -0,0 +1,9 @@ +#! /usr/bin/env bash + +set -e + +# run migrations +alembic upgrade head + +# start app +uvicorn app.main:app --reload --host ${HOST:-0.0.0.0} --port ${PORT:-80} diff --git a/app/tests/test_api.py b/app/tests/test_api.py new file mode 100644 index 0000000..22c59a5 --- /dev/null +++ b/app/tests/test_api.py @@ -0,0 +1,12 @@ +from fastapi.testclient import TestClient +from sqlalchemy.orm import Session + +from app.main import app +import app.db.models as models + +client = TestClient(app) + + +def test_create_task(db_session: Session) -> None: + jobs = db_session.query(models.Job).all() + assert len(jobs) == 0 diff --git a/dev.Dockerfile b/dev.Dockerfile new file mode 100644 index 0000000..0866c5b --- /dev/null +++ b/dev.Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11 + +WORKDIR /code + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Install dependencies +COPY pyproject.toml . +RUN pip install -U pip +RUN pip install .[test] + +# The source code is mounted as a volume at /code, no need to copy. + +ENTRYPOINT ["bash", "./app/start.sh"] diff --git a/dev.docker-compose.yml b/dev.docker-compose.yml new file mode 100644 index 0000000..7d8b19e --- /dev/null +++ b/dev.docker-compose.yml @@ -0,0 +1,55 @@ +version: "3.8" + +services: + app: + container_name: whisper_api_app + build: + context: . + dockerfile: dev.Dockerfile + environment: + DATABASE_URI: postgresql://postgres:postgres@postgres/whisper_api + ENVIRONMENT: development + API_SECRET: foobar + ports: + - "8000:80" + networks: + - app + volumes: + - ./:/code + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + + postgres: + container_name: whisper_api_postgres + image: postgres:15-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: whisper_api + ports: + - "5432:5432" + networks: + - app + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + container_name: whisper_api_redis + image: redis:7-alpine + networks: + - app + +volumes: + postgres-data: + +networks: + app: + driver: bridge diff --git a/pyproject.toml b/pyproject.toml index 251bc16..a5980f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,12 +17,12 @@ dev = [ # code formatting "black", "isort", - # linting "flake8", "mypy", +] - # tests +test = [ "httpx", "sqlalchemy-stubs", "sqlalchemy-utils", @@ -31,3 +31,6 @@ dev = [ [tool.isort] profile = "black" + +[tool.setuptools] +py-modules = []