Initial commit

This commit is contained in:
2026-06-01 16:48:21 +02:00
commit 53d157535a
159 changed files with 13641 additions and 0 deletions
@@ -0,0 +1,33 @@
"""Postgres stub for VacationDaysRepository.
Implement when switching to Postgres (install asyncpg via `uv sync --extra postgres`).
All methods raise NotImplementedError until then.
"""
from uuid import UUID
from app.entities.vacation_days import VacationDays
from app.repositories.vacation_days_repository import VacationDaysRepository
class PostgresVacationDaysRepository(VacationDaysRepository):
def __init__(self, connection: object) -> None:
self._conn = connection
async def create(self, item: VacationDays) -> VacationDays:
raise NotImplementedError("Postgres repo not yet implemented")
async def get_by_id(self, id: UUID) -> VacationDays | None:
raise NotImplementedError("Postgres repo not yet implemented")
async def update(self, id: UUID, item: VacationDays) -> VacationDays:
raise NotImplementedError("Postgres repo not yet implemented")
async def delete(self, id: UUID) -> None:
raise NotImplementedError("Postgres repo not yet implemented")
async def list(self, **filters: object) -> list[VacationDays]:
raise NotImplementedError("Postgres repo not yet implemented")
async def get_by_parent_year(self, personal_data_uuid: UUID, year: int) -> VacationDays | None:
raise NotImplementedError("Postgres repo not yet implemented")
@@ -0,0 +1,115 @@
"""SQLite implementation of BreakPeriodRepository.
Handles the break_period table with date fields (start_date, end_date).
"""
from datetime import date, datetime
from uuid import UUID
import aiosqlite
from app.entities.break_period import BreakPeriod
from app.repositories.break_period_repository import BreakPeriodRepository
from app.utils.errors import NotFoundError
class SQLiteBreakPeriodRepository(BreakPeriodRepository):
def __init__(self, connection: aiosqlite.Connection) -> None:
self._conn = connection
@staticmethod
def _row_to_entity(row: aiosqlite.Row) -> BreakPeriod:
return BreakPeriod(
id=UUID(row["id"]),
workspace_id=UUID(row["workspace_id"]),
name=row["name"],
start_date=date.fromisoformat(row["start_date"]),
end_date=date.fromisoformat(row["end_date"]),
target_weeks=row["target_weeks"],
created_at=datetime.fromisoformat(row["created_at"]),
updated_at=datetime.fromisoformat(row["updated_at"]),
)
async def create(self, item: BreakPeriod) -> BreakPeriod:
now = datetime.now()
item.created_at = item.created_at or now
item.updated_at = now
await self._conn.execute(
"""
INSERT INTO break_period
(id, workspace_id, name, start_date, end_date, target_weeks,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
str(item.id),
str(item.workspace_id),
item.name,
item.start_date.isoformat(),
item.end_date.isoformat(),
item.target_weeks,
item.created_at.isoformat(),
item.updated_at.isoformat(),
),
)
await self._conn.commit()
return item
async def get_by_id(self, id: UUID) -> BreakPeriod | None:
cursor = await self._conn.execute("SELECT * FROM break_period WHERE id = ?", (str(id),))
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def get_by_workspace(self, workspace_id: UUID) -> list[BreakPeriod]:
cursor = await self._conn.execute(
"SELECT * FROM break_period WHERE workspace_id = ? ORDER BY start_date ASC",
(str(workspace_id),),
)
rows = await cursor.fetchall()
return [self._row_to_entity(row) for row in rows]
async def update(self, id: UUID, item: BreakPeriod) -> BreakPeriod:
item.updated_at = datetime.now()
cursor = await self._conn.execute(
"""
UPDATE break_period
SET name = ?, start_date = ?, end_date = ?, target_weeks = ?,
updated_at = ?
WHERE id = ?
""",
(
item.name,
item.start_date.isoformat(),
item.end_date.isoformat(),
item.target_weeks,
item.updated_at.isoformat(),
str(id),
),
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"BreakPeriod {id} not found")
refreshed = await self.get_by_id(id)
assert refreshed is not None # just updated
return refreshed
async def delete(self, id: UUID) -> None:
cursor = await self._conn.execute("DELETE FROM break_period WHERE id = ?", (str(id),))
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"BreakPeriod {id} not found")
async def list(self, **filters: object) -> list[BreakPeriod]:
clauses: list[str] = []
params: list[object] = []
for column in ("workspace_id",):
if column in filters and filters[column] is not None:
value = filters[column]
clauses.append(f"{column} = ?")
params.append(str(value) if isinstance(value, UUID) else value)
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
cursor = await self._conn.execute(
f"SELECT * FROM break_period{where} ORDER BY start_date ASC", params
)
rows = await cursor.fetchall()
return [self._row_to_entity(row) for row in rows]
@@ -0,0 +1,97 @@
"""SQLite implementation of ChildRepository."""
from datetime import date, datetime
from uuid import UUID
import aiosqlite
from app.entities.child import Child
from app.repositories.child_repository import ChildRepository
from app.utils.errors import NotFoundError
class SQLiteChildRepository(ChildRepository):
def __init__(self, connection: aiosqlite.Connection) -> None:
self._conn = connection
@staticmethod
def _row_to_entity(row: aiosqlite.Row) -> Child:
return Child(
id=UUID(row["id"]),
workspace_id=UUID(row["workspace_id"]),
name=row["name"],
birth_date=date.fromisoformat(row["birth_date"]),
preschool_start_date=(
date.fromisoformat(row["preschool_start_date"])
if row["preschool_start_date"]
else None
),
created_at=datetime.fromisoformat(row["created_at"]),
)
async def create(self, item: Child) -> Child:
item.created_at = item.created_at or datetime.now()
await self._conn.execute(
"""
INSERT INTO child (id, workspace_id, name, birth_date, preschool_start_date, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
str(item.id),
str(item.workspace_id),
item.name,
item.birth_date.isoformat(),
item.preschool_start_date.isoformat() if item.preschool_start_date else None,
item.created_at.isoformat(),
),
)
await self._conn.commit()
return item
async def get_by_id(self, id: UUID) -> Child | None:
cursor = await self._conn.execute("SELECT * FROM child WHERE id = ?", (str(id),))
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def update(self, id: UUID, item: Child) -> Child:
cursor = await self._conn.execute(
"""
UPDATE child
SET name = ?, birth_date = ?, preschool_start_date = ?
WHERE id = ?
""",
(
item.name,
item.birth_date.isoformat(),
item.preschool_start_date.isoformat() if item.preschool_start_date else None,
str(id),
),
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"Child {id} not found")
refreshed = await self.get_by_id(id)
assert refreshed is not None
return refreshed
async def delete(self, id: UUID) -> None:
cursor = await self._conn.execute("DELETE FROM child WHERE id = ?", (str(id),))
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"Child {id} not found")
async def list(self, **filters: object) -> list[Child]:
clauses: list[str] = []
params: list[object] = []
if "workspace_id" in filters and filters["workspace_id"] is not None:
clauses.append("workspace_id = ?")
params.append(str(filters["workspace_id"]))
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
cursor = await self._conn.execute(
f"SELECT * FROM child{where} ORDER BY birth_date ASC", params
)
rows = await cursor.fetchall()
return [self._row_to_entity(r) for r in rows]
async def list_by_workspace(self, workspace_id: UUID) -> list[Child]:
return await self.list(workspace_id=workspace_id)
@@ -0,0 +1,179 @@
"""SQLite implementation of ParentalLeaveRepository."""
import json
from datetime import datetime
from uuid import UUID
import aiosqlite
from app.entities.parental_leave import ParentalLeaveFetch, TierData
from app.repositories.parental_leave_repository import ParentalLeaveRepository
from app.utils.errors import NotFoundError
class SQLiteParentalLeaveRepository(ParentalLeaveRepository):
def __init__(self, connection: aiosqlite.Connection) -> None:
self._conn = connection
@staticmethod
def _row_to_entity(row: aiosqlite.Row) -> ParentalLeaveFetch:
raw = json.loads(row["tier_data"])
tier_data = TierData(
sjukpenning_total=raw["sjukpenning_total"],
sjukpenning_used=raw["sjukpenning_used"],
sjukpenning_left=raw["sjukpenning_left"],
lagsta_niva_total=raw["lagsta_niva_total"],
lagsta_niva_used=raw["lagsta_niva_used"],
lagsta_niva_left=raw["lagsta_niva_left"],
)
return ParentalLeaveFetch(
id=UUID(row["id"]),
personal_data_uuid=UUID(row["personal_data_uuid"]),
workspace_id=UUID(row["workspace_id"]),
child_id=UUID(row["child_id"]),
source=row["source"],
fetched_at=datetime.fromisoformat(row["fetched_at"]),
total_days=row["total_days"],
days_used=row["days_used"],
days_left=row["days_left"],
reserved_days_total=row["reserved_days_total"],
reserved_days_left=row["reserved_days_left"],
transferable_days_left=row["transferable_days_left"],
dubbeldagar_left=row["dubbeldagar_left"],
tier_data=tier_data,
created_at=datetime.fromisoformat(row["created_at"]),
updated_at=datetime.fromisoformat(row["updated_at"]),
)
@staticmethod
def _tier_data_to_json(td: TierData) -> str:
return json.dumps(
{
"sjukpenning_total": td.sjukpenning_total,
"sjukpenning_used": td.sjukpenning_used,
"sjukpenning_left": td.sjukpenning_left,
"lagsta_niva_total": td.lagsta_niva_total,
"lagsta_niva_used": td.lagsta_niva_used,
"lagsta_niva_left": td.lagsta_niva_left,
}
)
async def create(self, item: ParentalLeaveFetch) -> ParentalLeaveFetch:
now = datetime.now()
item.created_at = item.created_at or now
item.updated_at = item.updated_at or now
await self._conn.execute(
"""
INSERT INTO parental_leave_fetch
(id, personal_data_uuid, workspace_id, child_id, source, fetched_at,
total_days, days_used, days_left, reserved_days_total, reserved_days_left,
transferable_days_left, dubbeldagar_left, tier_data, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
str(item.id),
str(item.personal_data_uuid),
str(item.workspace_id),
str(item.child_id),
item.source,
item.fetched_at.isoformat(),
item.total_days,
item.days_used,
item.days_left,
item.reserved_days_total,
item.reserved_days_left,
item.transferable_days_left,
item.dubbeldagar_left,
self._tier_data_to_json(item.tier_data),
item.created_at.isoformat(),
item.updated_at.isoformat(),
),
)
await self._conn.commit()
return item
async def get_by_id(self, id: UUID) -> ParentalLeaveFetch | None:
cursor = await self._conn.execute(
"SELECT * FROM parental_leave_fetch WHERE id = ?", (str(id),)
)
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def get_latest_by_parent(
self, personal_data_uuid: UUID, child_id: UUID
) -> ParentalLeaveFetch | None:
cursor = await self._conn.execute(
"""
SELECT * FROM parental_leave_fetch
WHERE personal_data_uuid = ? AND child_id = ?
ORDER BY fetched_at DESC
LIMIT 1
""",
(str(personal_data_uuid), str(child_id)),
)
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def list_history(
self, personal_data_uuid: UUID, child_id: UUID
) -> list[ParentalLeaveFetch]:
cursor = await self._conn.execute(
"""
SELECT * FROM parental_leave_fetch
WHERE personal_data_uuid = ? AND child_id = ?
ORDER BY fetched_at DESC
""",
(str(personal_data_uuid), str(child_id)),
)
rows = await cursor.fetchall()
return [self._row_to_entity(r) for r in rows]
async def update(self, id: UUID, item: ParentalLeaveFetch) -> ParentalLeaveFetch:
item.updated_at = datetime.now()
cursor = await self._conn.execute(
"""
UPDATE parental_leave_fetch
SET days_used = ?, days_left = ?, reserved_days_left = ?,
transferable_days_left = ?, dubbeldagar_left = ?,
tier_data = ?, updated_at = ?
WHERE id = ?
""",
(
item.days_used,
item.days_left,
item.reserved_days_left,
item.transferable_days_left,
item.dubbeldagar_left,
self._tier_data_to_json(item.tier_data),
item.updated_at.isoformat(),
str(id),
),
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"ParentalLeaveFetch {id} not found")
refreshed = await self.get_by_id(id)
assert refreshed is not None
return refreshed
async def delete(self, id: UUID) -> None:
cursor = await self._conn.execute(
"DELETE FROM parental_leave_fetch WHERE id = ?", (str(id),)
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"ParentalLeaveFetch {id} not found")
async def list(self, **filters: object) -> list[ParentalLeaveFetch]:
clauses: list[str] = []
params: list[object] = []
for column in ("personal_data_uuid", "workspace_id", "child_id"):
if column in filters and filters[column] is not None:
clauses.append(f"{column} = ?")
params.append(str(filters[column]))
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
cursor = await self._conn.execute(
f"SELECT * FROM parental_leave_fetch{where} ORDER BY fetched_at DESC", params
)
rows = await cursor.fetchall()
return [self._row_to_entity(r) for r in rows]
@@ -0,0 +1,114 @@
"""SQLite implementation of PersonalDataRepository.
Soft-delete: sets `deleted_at`; hard-delete: removes the row (CASCADE deletes
all child rows via FK constraints).
"""
from datetime import datetime
from uuid import UUID
import aiosqlite
from app.entities.personal_data import PersonalData
from app.repositories.personal_data_repository import PersonalDataRepository
from app.utils.errors import NotFoundError
class SQLitePersonalDataRepository(PersonalDataRepository):
def __init__(self, connection: aiosqlite.Connection) -> None:
self._conn = connection
@staticmethod
def _row_to_entity(row: aiosqlite.Row) -> PersonalData:
return PersonalData(
uuid=UUID(row["uuid"]),
workspace_id=UUID(row["workspace_id"]),
name=row["name"],
salary_type=row["salary_type"],
employment_status=row["employment_status"],
personnummer_enc=row["personnummer_enc"],
created_at=datetime.fromisoformat(row["created_at"]),
deleted_at=datetime.fromisoformat(row["deleted_at"]) if row["deleted_at"] else None,
)
async def create(self, item: PersonalData) -> PersonalData:
now = datetime.now()
item.created_at = item.created_at or now
await self._conn.execute(
"""
INSERT INTO personal_data
(uuid, workspace_id, name, salary_type, employment_status,
personnummer_enc, created_at, deleted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
str(item.uuid),
str(item.workspace_id),
item.name,
item.salary_type,
item.employment_status,
item.personnummer_enc,
item.created_at.isoformat(),
item.deleted_at.isoformat() if item.deleted_at else None,
),
)
await self._conn.commit()
return item
async def get_by_id(self, id: UUID) -> PersonalData | None:
cursor = await self._conn.execute(
"SELECT * FROM personal_data WHERE uuid = ? AND deleted_at IS NULL",
(str(id),),
)
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def update(self, id: UUID, item: PersonalData) -> PersonalData:
cursor = await self._conn.execute(
"""
UPDATE personal_data
SET name = ?, salary_type = ?, employment_status = ?,
personnummer_enc = ?
WHERE uuid = ? AND deleted_at IS NULL
""",
(item.name, item.salary_type, item.employment_status, item.personnummer_enc, str(id)),
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"PersonalData {id} not found")
refreshed = await self.get_by_id(id)
assert refreshed is not None
return refreshed
async def delete(self, id: UUID) -> None:
await self.soft_delete(id)
async def soft_delete(self, id: UUID) -> None:
cursor = await self._conn.execute(
"UPDATE personal_data SET deleted_at = ? WHERE uuid = ? AND deleted_at IS NULL",
(datetime.now().isoformat(), str(id)),
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"PersonalData {id} not found")
async def hard_delete(self, id: UUID) -> None:
cursor = await self._conn.execute("DELETE FROM personal_data WHERE uuid = ?", (str(id),))
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"PersonalData {id} not found")
async def list(self, **filters: object) -> list[PersonalData]:
clauses = ["deleted_at IS NULL"]
params: list[object] = []
for column in ("workspace_id",):
if column in filters and filters[column] is not None:
clauses.append(f"{column} = ?")
params.append(str(filters[column]))
where = "WHERE " + " AND ".join(clauses)
cursor = await self._conn.execute(f"SELECT * FROM personal_data {where}", params)
rows = await cursor.fetchall()
return [self._row_to_entity(r) for r in rows]
async def list_by_workspace(self, workspace_id: UUID) -> list[PersonalData]:
return await self.list(workspace_id=workspace_id)
@@ -0,0 +1,161 @@
"""SQLite implementation of PlannedLeaveRepository."""
from datetime import date, datetime
from uuid import UUID
import aiosqlite
from app.entities.planned_leave import PlannedLeaveDay
from app.repositories.planned_leave_repository import PlannedLeaveRepository
from app.utils.errors import NotFoundError
class SQLitePlannedLeaveRepository(PlannedLeaveRepository):
def __init__(self, connection: aiosqlite.Connection) -> None:
self._conn = connection
@staticmethod
def _row_to_entity(row: aiosqlite.Row) -> PlannedLeaveDay:
return PlannedLeaveDay(
id=UUID(row["id"]),
workspace_id=UUID(row["workspace_id"]),
child_id=UUID(row["child_id"]),
parent_personal_data_uuid=UUID(row["parent_personal_data_uuid"]),
date=date.fromisoformat(row["date"]),
day_type=row["day_type"],
is_dubbeldag=bool(row["is_dubbeldag"]),
created_at=datetime.fromisoformat(row["created_at"]),
updated_at=datetime.fromisoformat(row["updated_at"]),
)
async def create(self, item: PlannedLeaveDay) -> PlannedLeaveDay:
now = datetime.now()
item.created_at = item.created_at or now
item.updated_at = item.updated_at or now
await self._conn.execute(
"""
INSERT INTO planned_leave_day
(id, workspace_id, child_id, parent_personal_data_uuid, date,
day_type, is_dubbeldag, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
str(item.id),
str(item.workspace_id),
str(item.child_id),
str(item.parent_personal_data_uuid),
item.date.isoformat(),
item.day_type,
int(item.is_dubbeldag),
item.created_at.isoformat(),
item.updated_at.isoformat(),
),
)
await self._conn.commit()
return item
async def get_by_id(self, id: UUID) -> PlannedLeaveDay | None:
cursor = await self._conn.execute(
"SELECT * FROM planned_leave_day WHERE id = ?", (str(id),)
)
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def update(self, id: UUID, item: PlannedLeaveDay) -> PlannedLeaveDay:
item.updated_at = datetime.now()
cursor = await self._conn.execute(
"""
UPDATE planned_leave_day
SET day_type = ?, is_dubbeldag = ?, updated_at = ?
WHERE id = ?
""",
(item.day_type, int(item.is_dubbeldag), item.updated_at.isoformat(), str(id)),
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"PlannedLeaveDay {id} not found")
refreshed = await self.get_by_id(id)
assert refreshed is not None
return refreshed
async def delete(self, id: UUID) -> None:
cursor = await self._conn.execute("DELETE FROM planned_leave_day WHERE id = ?", (str(id),))
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"PlannedLeaveDay {id} not found")
async def list(self, **filters: object) -> list[PlannedLeaveDay]:
clauses: list[str] = []
params: list[object] = []
for column in ("workspace_id", "child_id", "parent_personal_data_uuid"):
if column in filters and filters[column] is not None:
clauses.append(f"{column} = ?")
params.append(str(filters[column]))
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
cursor = await self._conn.execute(
f"SELECT * FROM planned_leave_day{where} ORDER BY date ASC", params
)
rows = await cursor.fetchall()
return [self._row_to_entity(r) for r in rows]
async def list_by_workspace_child(
self,
workspace_id: UUID,
child_id: UUID,
from_date: date | None = None,
to_date: date | None = None,
) -> list[PlannedLeaveDay]:
clauses = ["workspace_id = ?", "child_id = ?"]
params: list[object] = [str(workspace_id), str(child_id)]
if from_date:
clauses.append("date >= ?")
params.append(from_date.isoformat())
if to_date:
clauses.append("date <= ?")
params.append(to_date.isoformat())
cursor = await self._conn.execute(
f"SELECT * FROM planned_leave_day WHERE {' AND '.join(clauses)} ORDER BY date ASC",
params,
)
rows = await cursor.fetchall()
return [self._row_to_entity(r) for r in rows]
async def count_by_parent_type(
self,
personal_data_uuid: UUID,
child_id: UUID,
day_type: str,
from_date: date | None = None,
to_date: date | None = None,
) -> int:
clauses = [
"parent_personal_data_uuid = ?",
"child_id = ?",
"day_type = ?",
]
params: list[object] = [str(personal_data_uuid), str(child_id), day_type]
if from_date:
clauses.append("date >= ?")
params.append(from_date.isoformat())
if to_date:
clauses.append("date <= ?")
params.append(to_date.isoformat())
cursor = await self._conn.execute(
f"SELECT COUNT(*) FROM planned_leave_day WHERE {' AND '.join(clauses)}",
params,
)
row = await cursor.fetchone()
return int(row[0]) if row else 0
async def count_dubbeldagar(self, child_id: UUID, from_date: date | None = None) -> int:
clauses = ["child_id = ?", "is_dubbeldag = 1"]
params: list[object] = [str(child_id)]
if from_date:
clauses.append("date >= ?")
params.append(from_date.isoformat())
cursor = await self._conn.execute(
f"SELECT COUNT(*) FROM planned_leave_day WHERE {' AND '.join(clauses)}",
params,
)
row = await cursor.fetchone()
return int(row[0]) if row else 0
@@ -0,0 +1,77 @@
"""SQLite implementation of UserRepository."""
from datetime import datetime
from uuid import UUID
import aiosqlite
from app.entities.user import User
from app.repositories.user_repository import UserRepository
from app.utils.errors import NotFoundError
class SQLiteUserRepository(UserRepository):
def __init__(self, connection: aiosqlite.Connection) -> None:
self._conn = connection
@staticmethod
def _row_to_entity(row: aiosqlite.Row) -> User:
return User(
id=UUID(row["id"]),
personal_data_uuid=UUID(row["personal_data_uuid"]),
email=row["email"],
hashed_password=row["hashed_password"],
created_at=datetime.fromisoformat(row["created_at"]),
)
async def create(self, item: User) -> User:
now = datetime.now()
item.created_at = item.created_at or now
await self._conn.execute(
"""
INSERT INTO "user" (id, personal_data_uuid, email, hashed_password, created_at)
VALUES (?, ?, ?, ?, ?)
""",
(
str(item.id),
str(item.personal_data_uuid),
item.email,
item.hashed_password,
item.created_at.isoformat(),
),
)
await self._conn.commit()
return item
async def get_by_id(self, id: UUID) -> User | None:
cursor = await self._conn.execute('SELECT * FROM "user" WHERE id = ?', (str(id),))
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def get_by_email(self, email: str) -> User | None:
cursor = await self._conn.execute('SELECT * FROM "user" WHERE email = ?', (email,))
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def update(self, id: UUID, item: User) -> User:
cursor = await self._conn.execute(
'UPDATE "user" SET email = ?, hashed_password = ? WHERE id = ?',
(item.email, item.hashed_password, str(id)),
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"User {id} not found")
refreshed = await self.get_by_id(id)
assert refreshed is not None
return refreshed
async def delete(self, id: UUID) -> None:
cursor = await self._conn.execute('DELETE FROM "user" WHERE id = ?', (str(id),))
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"User {id} not found")
async def list(self, **filters: object) -> list[User]:
cursor = await self._conn.execute('SELECT * FROM "user"')
rows = await cursor.fetchall()
return [self._row_to_entity(r) for r in rows]
@@ -0,0 +1,111 @@
"""SQLite implementation of VacationDaysRepository.
REFERENCE implementation — copy this structure for other entities' SQLite repos.
Notes for whoever extends this:
- Map rows <-> entities explicitly in `_row_to_entity` (no ORM magic).
- Always parameterize queries (never string-format SQL).
- Raise NotFoundError from update/delete when the row is missing.
"""
from datetime import datetime
from uuid import UUID
import aiosqlite
from app.entities.vacation_days import VacationDays
from app.repositories.vacation_days_repository import VacationDaysRepository
from app.utils.errors import NotFoundError
class SQLiteVacationDaysRepository(VacationDaysRepository):
def __init__(self, connection: aiosqlite.Connection) -> None:
self._conn = connection
@staticmethod
def _row_to_entity(row: aiosqlite.Row) -> VacationDays:
return VacationDays(
id=UUID(row["id"]),
personal_data_uuid=UUID(row["personal_data_uuid"]),
workspace_id=UUID(row["workspace_id"]),
year=row["year"],
total_days=row["total_days"],
saved_days_in=row["saved_days_in"],
created_at=datetime.fromisoformat(row["created_at"]),
updated_at=datetime.fromisoformat(row["updated_at"]),
)
async def create(self, item: VacationDays) -> VacationDays:
now = datetime.now()
item.created_at = item.created_at or now
item.updated_at = now
await self._conn.execute(
"""
INSERT INTO vacation_days
(id, personal_data_uuid, workspace_id, year, total_days,
saved_days_in, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
str(item.id),
str(item.personal_data_uuid),
str(item.workspace_id),
item.year,
item.total_days,
item.saved_days_in,
item.created_at.isoformat(),
item.updated_at.isoformat(),
),
)
await self._conn.commit()
return item
async def get_by_id(self, id: UUID) -> VacationDays | None:
cursor = await self._conn.execute("SELECT * FROM vacation_days WHERE id = ?", (str(id),))
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def get_by_parent_year(self, personal_data_uuid: UUID, year: int) -> VacationDays | None:
cursor = await self._conn.execute(
"SELECT * FROM vacation_days WHERE personal_data_uuid = ? AND year = ?",
(str(personal_data_uuid), year),
)
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def update(self, id: UUID, item: VacationDays) -> VacationDays:
item.updated_at = datetime.now()
cursor = await self._conn.execute(
"""
UPDATE vacation_days
SET total_days = ?, saved_days_in = ?, updated_at = ?
WHERE id = ?
""",
(item.total_days, item.saved_days_in, item.updated_at.isoformat(), str(id)),
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"VacationDays {id} not found")
refreshed = await self.get_by_id(id)
assert refreshed is not None # just updated
return refreshed
async def delete(self, id: UUID) -> None:
cursor = await self._conn.execute("DELETE FROM vacation_days WHERE id = ?", (str(id),))
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"VacationDays {id} not found")
async def list(self, **filters: object) -> list[VacationDays]:
clauses: list[str] = []
params: list[object] = []
for column in ("workspace_id", "personal_data_uuid", "year"):
if column in filters and filters[column] is not None:
value = filters[column]
clauses.append(f"{column} = ?")
params.append(str(value) if isinstance(value, UUID) else value)
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
cursor = await self._conn.execute(
f"SELECT * FROM vacation_days{where} ORDER BY year DESC", params
)
rows = await cursor.fetchall()
return [self._row_to_entity(row) for row in rows]
@@ -0,0 +1,168 @@
"""SQLite implementation of WorkspaceRepository and WorkspaceInvitationRepository."""
from datetime import datetime
from uuid import UUID
import aiosqlite
from app.entities.workspace import Workspace
from app.entities.workspace_invitation import WorkspaceInvitation
from app.repositories.workspace_repository import (
WorkspaceInvitationRepository,
WorkspaceRepository,
)
from app.utils.errors import NotFoundError
class SQLiteWorkspaceRepository(WorkspaceRepository):
def __init__(self, connection: aiosqlite.Connection) -> None:
self._conn = connection
@staticmethod
def _row_to_entity(row: aiosqlite.Row) -> Workspace:
return Workspace(
id=UUID(row["id"]),
name=row["name"],
planning_goal=row["planning_goal"],
created_at=datetime.fromisoformat(row["created_at"]),
)
async def create(self, item: Workspace) -> Workspace:
item.created_at = item.created_at or datetime.now()
await self._conn.execute(
"INSERT INTO workspace (id, name, planning_goal, created_at) VALUES (?, ?, ?, ?)",
(str(item.id), item.name, item.planning_goal, item.created_at.isoformat()),
)
await self._conn.commit()
return item
async def get_by_id(self, id: UUID) -> Workspace | None:
cursor = await self._conn.execute("SELECT * FROM workspace WHERE id = ?", (str(id),))
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def update(self, id: UUID, item: Workspace) -> Workspace:
cursor = await self._conn.execute(
"UPDATE workspace SET name = ?, planning_goal = ? WHERE id = ?",
(item.name, item.planning_goal, str(id)),
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"Workspace {id} not found")
refreshed = await self.get_by_id(id)
assert refreshed is not None
return refreshed
async def delete(self, id: UUID) -> None:
cursor = await self._conn.execute("DELETE FROM workspace WHERE id = ?", (str(id),))
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"Workspace {id} not found")
async def list(self, **filters: object) -> list[Workspace]:
cursor = await self._conn.execute("SELECT * FROM workspace")
rows = await cursor.fetchall()
return [self._row_to_entity(r) for r in rows]
class SQLiteWorkspaceInvitationRepository(WorkspaceInvitationRepository):
def __init__(self, connection: aiosqlite.Connection) -> None:
self._conn = connection
@staticmethod
def _row_to_entity(row: aiosqlite.Row) -> WorkspaceInvitation:
return WorkspaceInvitation(
id=UUID(row["id"]),
workspace_id=UUID(row["workspace_id"]),
token_hash=row["token_hash"],
invited_by_user_id=UUID(row["invited_by_user_id"]),
status=row["status"],
expires_at=datetime.fromisoformat(row["expires_at"]),
created_at=datetime.fromisoformat(row["created_at"]),
accepted_at=datetime.fromisoformat(row["accepted_at"]) if row["accepted_at"] else None,
)
async def create(self, item: WorkspaceInvitation) -> WorkspaceInvitation:
await self._conn.execute(
"""
INSERT INTO workspace_invitation
(id, workspace_id, token_hash, invited_by_user_id, status,
expires_at, created_at, accepted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
str(item.id),
str(item.workspace_id),
item.token_hash,
str(item.invited_by_user_id),
item.status,
item.expires_at.isoformat(),
item.created_at.isoformat(),
item.accepted_at.isoformat() if item.accepted_at else None,
),
)
await self._conn.commit()
return item
async def get_by_id(self, id: UUID) -> WorkspaceInvitation | None:
cursor = await self._conn.execute(
"SELECT * FROM workspace_invitation WHERE id = ?", (str(id),)
)
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def get_by_token_hash(self, token_hash: str) -> WorkspaceInvitation | None:
cursor = await self._conn.execute(
"SELECT * FROM workspace_invitation WHERE token_hash = ?", (token_hash,)
)
row = await cursor.fetchone()
return self._row_to_entity(row) if row else None
async def update(self, id: UUID, item: WorkspaceInvitation) -> WorkspaceInvitation:
cursor = await self._conn.execute(
"""
UPDATE workspace_invitation
SET status = ?, accepted_at = ?
WHERE id = ?
""",
(
item.status,
item.accepted_at.isoformat() if item.accepted_at else None,
str(id),
),
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"WorkspaceInvitation {id} not found")
refreshed = await self.get_by_id(id)
assert refreshed is not None
return refreshed
async def delete(self, id: UUID) -> None:
cursor = await self._conn.execute(
"DELETE FROM workspace_invitation WHERE id = ?", (str(id),)
)
await self._conn.commit()
if cursor.rowcount == 0:
raise NotFoundError(f"WorkspaceInvitation {id} not found")
async def list(self, **filters: object) -> list[WorkspaceInvitation]:
clauses: list[str] = []
params: list[object] = []
if "workspace_id" in filters and filters["workspace_id"] is not None:
clauses.append("workspace_id = ?")
params.append(str(filters["workspace_id"]))
if "status" in filters and filters["status"] is not None:
clauses.append("status = ?")
params.append(filters["status"])
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
cursor = await self._conn.execute(
f"SELECT * FROM workspace_invitation{where} ORDER BY created_at DESC", params
)
rows = await cursor.fetchall()
return [self._row_to_entity(r) for r in rows]
async def list_by_workspace(
self, workspace_id: UUID, status: str | None = None
) -> list[WorkspaceInvitation]:
return await self.list(workspace_id=workspace_id, status=status)