From b64c8c008df8b230df14bd56316f76f00d4fb374 Mon Sep 17 00:00:00 2001 From: Bill Date: Sun, 30 Nov 2025 15:11:29 -0500 Subject: [PATCH] feat: add job models with Pydantic --- app/models.py | 48 ++++++++++++++++++ requirements.txt | 6 +-- tests/__init__.py | 0 tests/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 144 bytes .../test_models.cpython-314-pytest-9.0.1.pyc | Bin 0 -> 11473 bytes tests/test_models.py | 31 +++++++++++ 6 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 app/models.py create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-314.pyc create mode 100644 tests/__pycache__/test_models.cpython-314-pytest-9.0.1.pyc create mode 100644 tests/test_models.py diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..023f20b --- /dev/null +++ b/app/models.py @@ -0,0 +1,48 @@ +import secrets +from datetime import datetime, timezone +from enum import Enum + +from pydantic import BaseModel, Field + + +class JobStatus(str, Enum): + QUEUED = "queued" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + + +class Progress(BaseModel): + frame: int = 0 + fps: float = 0.0 + time: str = "00:00:00.00" + bitrate: str = "0kbits/s" + percent: float | None = None + + +class Job(BaseModel): + id: str = Field(default_factory=lambda: f"job_{secrets.token_hex(8)}") + status: JobStatus = JobStatus.QUEUED + command: str + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + started_at: datetime | None = None + completed_at: datetime | None = None + progress: Progress | None = None + output_files: list[str] = Field(default_factory=list) + error: str | None = None + + +class CreateJobRequest(BaseModel): + command: str + + +class JobResponse(BaseModel): + id: str + status: JobStatus + command: str + created_at: datetime + started_at: datetime | None = None + completed_at: datetime | None = None + progress: Progress | None = None + output_files: list[str] = Field(default_factory=list) + error: str | None = None diff --git a/requirements.txt b/requirements.txt index 165ed8a..b98e9b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -fastapi==0.115.0 -uvicorn[standard]==0.32.0 -pydantic==2.9.0 +fastapi==0.123.0 +uvicorn[standard]==0.38.0 +pydantic==2.12.5 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/__init__.cpython-314.pyc b/tests/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..41aae9a794b549b075dc5ebab090f8071fd31148 GIT binary patch literal 144 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08COIJT5KQ~oB zDJd~0C%;rbEiJboHC?wnzbHGkNWUbtxTIJ=K0Y%qvm`!Vub}c4hfQvNN@-52T@fo# TH^}^A5aSawBO_xGGmr%UBibNG literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_models.cpython-314-pytest-9.0.1.pyc b/tests/__pycache__/test_models.cpython-314-pytest-9.0.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..79eeefec0f9111ee30b40be7d4e1330331184efc GIT binary patch literal 11473 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 literal 0 HcmV?d00001 diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..0189ca7 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,31 @@ +from datetime import datetime + +from app.models import Job, JobStatus, CreateJobRequest, JobResponse + + +def test_job_creation(): + job = Job(command="-i input.mp4 output.mp4") + + assert job.id.startswith("job_") + assert len(job.id) == 20 # job_ + 16 hex chars + assert job.status == JobStatus.QUEUED + assert job.command == "-i input.mp4 output.mp4" + assert isinstance(job.created_at, datetime) + assert job.started_at is None + assert job.completed_at is None + assert job.progress is None + assert job.output_files == [] + assert job.error is None + + +def test_create_job_request(): + request = CreateJobRequest(command="-i test.mp4 out.mp4") + assert request.command == "-i test.mp4 out.mp4" + + +def test_job_response_from_job(): + job = Job(command="-i input.mp4 output.mp4") + response = JobResponse.model_validate(job.model_dump()) + + assert response.id == job.id + assert response.status == JobStatus.QUEUED