feat(taskflow): add core task API, storage persistence, csv export, stats page, and test coverage

This commit is contained in:
Alexander Kalinovsky
2026-04-01 17:56:03 +03:00
commit 19d659df6b
31 changed files with 4197 additions and 0 deletions

22
app/api/dto/__init__.py Normal file
View File

@@ -0,0 +1,22 @@
from app.api.dto.tasks import (
ErrorDTO,
TaskCreateDTO,
TaskDTO,
TaskExportRowDTO,
TaskListDTO,
TaskListItemDTO,
TaskQueryDTO,
TaskUpdateDTO,
)
__all__ = [
"ErrorDTO",
"TaskCreateDTO",
"TaskDTO",
"TaskExportRowDTO",
"TaskListDTO",
"TaskListItemDTO",
"TaskQueryDTO",
"TaskUpdateDTO",
]

142
app/api/dto/tasks.py Normal file
View File

@@ -0,0 +1,142 @@
from __future__ import annotations
from datetime import datetime
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, computed_field, field_validator
TaskStatus = Literal["backlog", "in_progress", "done"]
class ErrorDTO(BaseModel):
error: str = Field(..., examples=["invalid_id"])
message: str = Field(..., examples=["Task was not found"])
class TaskCreateDTO(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str = Field(
...,
min_length=1,
max_length=100,
description="Task title",
examples=["Implement CSV export"],
)
@field_validator("title")
@classmethod
def validate_title(cls, value: str) -> str:
value = value.strip()
if not value:
raise ValueError("Title must not be empty")
return value
class TaskUpdateDTO(BaseModel):
model_config = ConfigDict(extra="forbid")
title: str | None = Field(
default=None,
min_length=1,
max_length=100,
description="Updated task title",
examples=["Fix workflow metrics"],
)
@field_validator("title")
@classmethod
def validate_title(cls, value: str | None) -> str | None:
if value is None:
return None
value = value.strip()
if not value:
raise ValueError("Title must not be empty")
return value
class TaskDTO(BaseModel):
model_config = ConfigDict(
extra="forbid",
frozen=True,
populate_by_name=True,
)
id: UUID
title: str = Field(..., min_length=1, max_length=100)
created_at: datetime
started_at: datetime | None = None
done_at: datetime | None = None
@computed_field(return_type=str)
@property
def status(self) -> TaskStatus:
if self.done_at is not None:
return "done"
if self.started_at is not None:
return "in_progress"
return "backlog"
class TaskListItemDTO(BaseModel):
model_config = ConfigDict(
extra="forbid",
frozen=True,
populate_by_name=True,
)
id: UUID
title: str = Field(..., min_length=1, max_length=100)
created_at: datetime
started_at: datetime | None = None
done_at: datetime | None = None
@computed_field(return_type=str)
@property
def status(self) -> TaskStatus:
if self.done_at is not None:
return "done"
if self.started_at is not None:
return "in_progress"
return "backlog"
class TaskListDTO(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
items: list[TaskListItemDTO]
total: int = Field(..., ge=0)
limit: int = Field(..., ge=1, le=1000)
offset: int = Field(..., ge=0)
class TaskQueryDTO(BaseModel):
model_config = ConfigDict(extra="forbid")
limit: int = Field(default=100, ge=1, le=1000)
offset: int = Field(default=0, ge=0)
status: TaskStatus | None = Field(default=None)
search: str | None = Field(default=None, max_length=100)
@field_validator("search")
@classmethod
def normalize_search(cls, value: str | None) -> str | None:
if value is None:
return None
value = value.strip()
return value or None
class TaskExportRowDTO(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
id: UUID
title: str
status: TaskStatus
created_at: datetime
started_at: datetime | None = None
done_at: datetime | None = None