Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add storage and system card service for writing results away #24

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fe48735
First setup for task cards
uittenbroekrobbert May 8, 2024
41a8ab4
Refactor and extend tasks and statuses
uittenbroekrobbert May 13, 2024
26003ef
Refactor and extend tasks and statuses
uittenbroekrobbert May 13, 2024
73b075c
Apply suggestions from code review
uittenbroekrobbert May 14, 2024
7bdb279
Fix static files settings
uittenbroekrobbert May 14, 2024
5420f81
Template fix
uittenbroekrobbert May 14, 2024
63dc3a3
Fix form using json
uittenbroekrobbert May 14, 2024
9cf3a8c
Fixes after rebase
uittenbroekrobbert May 14, 2024
32663ca
Adding tables for databases
uittenbroekrobbert May 14, 2024
b4257ba
Adding more structure and database support
uittenbroekrobbert May 15, 2024
dad1fc6
Adding more structure and database support
uittenbroekrobbert May 15, 2024
ff5d50e
Adding more structure and database support
uittenbroekrobbert May 17, 2024
71bff01
Adding more structure and database support
uittenbroekrobbert May 23, 2024
cc4311c
add tests for pages and move task from api
uittenbroekrobbert May 24, 2024
98492b5
add tests for pages and move task from api
uittenbroekrobbert May 27, 2024
f135f48
add tests for pages and move task from api
uittenbroekrobbert May 27, 2024
20a9934
add tests for pages and move task from api
uittenbroekrobbert May 27, 2024
4beebdc
server_start returns location of the server
uittenbroekrobbert May 27, 2024
c67de8d
fix no results method and test for statuses
uittenbroekrobbert May 27, 2024
ab04533
add test for task repository
uittenbroekrobbert May 27, 2024
faaa7f1
add first test for task service
uittenbroekrobbert May 27, 2024
dcbb6c5
add test for task service
uittenbroekrobbert May 28, 2024
ca5fd98
add test for task service
uittenbroekrobbert May 28, 2024
105374d
Add initial files for writing away to a system card
laurensWe May 27, 2024
0f0a183
Add unittest
laurensWe May 28, 2024
ee36431
Refactor writer to storage and implement the system card service
laurensWe May 28, 2024
6706ba9
Ignore TRY003 (too long exception message) globally
laurensWe May 28, 2024
1f5d706
Change init in test to fixture
ChristopherSpelt May 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ PROJECT_NAME="TAD"
# TAD backend
BACKEND_CORS_ORIGINS="http://localhost,https://localhost,http://127.0.0.1,https://127.0.0.1"
SECRET_KEY=changethis
APP_DATABASE_SCHEME="postgresql"
APP_DATABASE_SCHEME="sqlite"
APP_DATABASE_USER=tad
APP_DATABASE_DB=tad
APP_DATABASE_PASSWORD=changethis
Expand Down
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ POSTGRES_PASSWORD=changethis

# Database viewer
PGADMIN_DEFAULT_PASSWORD=changethis

APP_DATABASE_FILE=database.sqlite3
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ __pypackages__/

#mypyr
.mypy_cache/
/.idea/

# macos
.DS_Store
Expand Down
Binary file added database.sqlite3.test
Binary file not shown.
770 changes: 496 additions & 274 deletions poetry.lock

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ jinja2 = "^3.1.4"
pydantic-settings = "^2.2.1"
psycopg2-binary = "^2.9.9"
uvicorn = {extras = ["standard"], version = "^0.29.0"}
playwright = "^1.44.0"
pytest-playwright = "^0.5.0"
pyyaml = "^6.0.1"

[tool.poetry.group.test.dependencies]
pytest = "^8.1.1"
Expand Down Expand Up @@ -63,6 +66,7 @@ line-ending = "lf"
select = ["I", "SIM", "B", "UP", "F", "E", "S", "C90", "DTZ", "LOG", "PIE", "PT", "ERA", "W", "C", "TRY", "RUF"]
fixable = ["ALL"]
task-tags = ["TODO"]
ignore = ["TRY003"]

[tool.ruff.lint.per-file-ignores]
"tests/**.py" = ["S101"]
Expand All @@ -77,7 +81,8 @@ reportMissingImports = true
reportMissingTypeStubs = true
reportUnnecessaryIsInstance = false
exclude = [
"tad/migrations"
"tad/migrations",
".venv"
]

[tool.coverage.run]
Expand Down
4 changes: 3 additions & 1 deletion tad/api/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from fastapi import APIRouter

from tad.api.routes import health, root
from tad.api.routes import health, pages, root, tasks

api_router = APIRouter()
api_router.include_router(root.router)
api_router.include_router(health.router, prefix="/health", tags=["health"])
api_router.include_router(pages.router, prefix="/pages", tags=["pages"])
api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
25 changes: 25 additions & 0 deletions tad/api/routes/pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from typing import Annotated

from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

from tad.services.statuses import StatusesService
from tad.services.tasks import TasksService

router = APIRouter()
templates = Jinja2Templates(directory="tad/site/templates")


@router.get("/", response_class=HTMLResponse)
async def default_layout(
request: Request,
status_service: Annotated[StatusesService, Depends(StatusesService)],
tasks_service: Annotated[TasksService, Depends(TasksService)],
):
context = {
"page_title": "This is the page title",
"tasks_service": tasks_service,
"statuses_service": status_service,
}
return templates.TemplateResponse(request=request, name="default_layout.jinja", context=context)
2 changes: 1 addition & 1 deletion tad/api/routes/root.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse

from tad.api.deps import templates
from tad.repositories.deps import templates

router = APIRouter()

Expand Down
48 changes: 48 additions & 0 deletions tad/api/routes/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import Annotated, Any

from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

from tad.models.task import MoveTask
from tad.services.tasks import TasksService

router = APIRouter()
templates = Jinja2Templates(directory="tad/site/templates")


@router.post("/move", response_class=HTMLResponse)
async def move_task(
request: Request, move_task: MoveTask, tasks_service: Annotated[TasksService, Depends(TasksService)]
) -> HTMLResponse:
"""
Move a task through an API call.
:param tasks_service: the task service
:param request: the request object
:param move_task: the move task object
:return: a HTMLResponse object, in this case the html code of the card that was moved
"""
try:
task = tasks_service.move_task(
convert_to_int_if_is_int(move_task.id),
convert_to_int_if_is_int(move_task.status_id),
convert_to_int_if_is_int(move_task.previous_sibling_id),
convert_to_int_if_is_int(move_task.next_sibling_id),
)
# todo(Robbert) add error handling for input error or task error handling
return templates.TemplateResponse(request=request, name="task.jinja", context={"task": task})
except Exception:
return templates.TemplateResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, request=request, name="error.jinja"
)


def convert_to_int_if_is_int(value: Any) -> int | Any:
"""
If the given value is of type int, return it as int, otherwise return the input value as is.
:param value: the value to convert
:return: the value as int or the original type
"""
if value is not None and isinstance(value, str) and value.isdigit():
return int(value)
return value
6 changes: 4 additions & 2 deletions tad/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# Self type is not available in Python 3.10 so create our own with TypeVar
SelfSettings = TypeVar("SelfSettings", bound="Settings")

logger = logging.getLogger(__name__)


class Settings(BaseSettings):
# todo(berry): investigate yaml, toml or json file support for SettingsConfigDict
Expand Down Expand Up @@ -42,7 +44,7 @@ def server_host(self) -> str:
PROJECT_NAME: str = "TAD"
PROJECT_DESCRIPTION: str = "Transparency of Algorithmic Decision making"

STATIC_DIR: str = "tad/static"
STATIC_DIR: str = "tad/site/static/"
TEMPLATE_DIR: str = "tad/templates"

# todo(berry): create submodel for database settings
Expand Down Expand Up @@ -85,7 +87,7 @@ def SQLALCHEMY_DATABASE_URI(self) -> str:
@model_validator(mode="after")
def _enforce_database_rules(self: SelfSettings) -> SelfSettings:
if self.ENVIRONMENT != "local" and self.APP_DATABASE_SCHEME == "sqlite":
raise SettingsError("SQLite is not supported in production") # noqa: TRY003
raise SettingsError("SQLite is not supported in production")
return self


Expand Down
12 changes: 10 additions & 2 deletions tad/core/db.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from sqlalchemy.engine.base import Engine
from sqlmodel import Session, create_engine, select

from tad.core.config import settings

engine = create_engine(settings.SQLALCHEMY_DATABASE_URI)
_engine: None | Engine = None


def get_engine() -> Engine:
global _engine
if _engine is None:
_engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, echo=True, connect_args={"check_same_thread": False})
return _engine


async def check_db():
with Session(engine) as session:
with Session(get_engine()) as session:
session.exec(select(1))
7 changes: 5 additions & 2 deletions tad/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
validation_exception_handler as tad_validation_exception_handler,
)
from tad.core.log import configure_logging
from tad.middleware.route_logging import RequestLoggingMiddleware
from tad.utils.mask import Mask

from .middleware.route_logging import RequestLoggingMiddleware

configure_logging(settings.LOGGING_LEVEL, settings.LOGGING_CONFIG)


logger = logging.getLogger(__name__)
mask = Mask(mask_keywords=["database_uri"])

Expand Down Expand Up @@ -52,7 +54,6 @@ async def lifespan(app: FastAPI):
)

app.add_middleware(RequestLoggingMiddleware)

app.mount("/static", StaticFiles(directory=settings.STATIC_DIR), name="static")


Expand All @@ -67,3 +68,5 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE


app.include_router(api_router)

# todo (robbert) add init code for example tasks and statuses
2 changes: 1 addition & 1 deletion tad/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def run_migrations_online():
"""
configuration = config.get_section(config.config_ini_section)
if configuration is None:
raise Exception("Failed to get configuration section") # noqa: TRY003, TRY002
raise Exception("Failed to get configuration section") # noqa: TRY002
configuration["sqlalchemy.url"] = get_url()
connectable = engine_from_config(
configuration,
Expand Down
67 changes: 67 additions & 0 deletions tad/migrations/versions/eb2eed884ae9_a_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""a message

Revision ID: eb2eed884ae9
Revises:
Create Date: 2024-05-14 13:36:23.551663

"""

from collections.abc import Sequence

import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "eb2eed884ae9"
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"hero",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"status",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("sort_order", sa.Float(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"user",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("avatar", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"task",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("sort_order", sa.Float(), nullable=False),
sa.Column("status_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("task")
op.drop_table("user")
op.drop_table("status")
op.drop_table("hero")
# ### end Alembic commands ###
5 changes: 4 additions & 1 deletion tad/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from .hero import Hero
from .status import Status
from .task import Task
from .user import User

__all__ = ["Hero"]
__all__ = ["Hero", "Task", "Status", "User"]
7 changes: 7 additions & 0 deletions tad/models/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from sqlmodel import Field, SQLModel


class Status(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
name: str
sort_order: float
30 changes: 30 additions & 0 deletions tad/models/task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from pydantic import BaseModel, ValidationInfo, field_validator
from pydantic import Field as PydanticField
from sqlmodel import Field as SQLField
from sqlmodel import SQLModel


class Task(SQLModel, table=True):
id: int = SQLField(default=None, primary_key=True)
title: str
description: str
sort_order: float
status_id: int | None = SQLField(default=None, foreign_key="status.id")
user_id: int | None = SQLField(default=None, foreign_key="user.id")
# todo(robbert) Tasks probably are grouped (and sub-grouped), so we probably need a reference to a group_id


class MoveTask(BaseModel):
# todo(robbert) values from htmx json are all strings, using type int does not work for
# sibling variables (they are optional)
id: str = PydanticField(None, alias="taskId", strict=False)
status_id: str = PydanticField(None, alias="statusId", strict=False)
previous_sibling_id: str | None = PydanticField(None, alias="previousSiblingId", strict=False)
next_sibling_id: str | None = PydanticField(None, alias="nextSiblingId", strict=False)

@field_validator("id", "status_id", "previous_sibling_id", "next_sibling_id")
@classmethod
def check_is_int(cls, value: str, info: ValidationInfo) -> str:
if isinstance(value, str) and value.isdigit():
assert value.isdigit(), f"{info.field_name} must be an integer" # noqa: S101
return value
7 changes: 7 additions & 0 deletions tad/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from sqlmodel import Field, SQLModel


class User(SQLModel, table=True):
id: int = Field(default=None, primary_key=True)
name: str
avatar: str | None
File renamed without changes.
11 changes: 3 additions & 8 deletions tad/api/deps.py → tad/repositories/deps.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
from collections.abc import Generator
from typing import Annotated

from fastapi import Depends
from fastapi.templating import Jinja2Templates
from sqlmodel import Session

from tad.core.config import settings
from tad.core.db import engine
from tad.core.db import get_engine

templates = Jinja2Templates(directory=settings.TEMPLATE_DIR)


def get_db() -> Generator[Session, None, None]:
with Session(engine) as session:
def get_session() -> Generator[Session, None, None]:
with Session(get_engine()) as session:
yield session


SessionDep = Annotated[Session, Depends(get_db)]
8 changes: 8 additions & 0 deletions tad/repositories/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from tad.core.exceptions import TADError


class RepositoryError(TADError):
def __init__(self, message: str = "Repository error"):
self.message: str = message
exception_name: str = self.__class__.__name__
super().__init__(f"{exception_name}: {self.message}")
Loading