tiago/db_structure #2

Merged
tscholten merged 7 commits from tiago/db_structure into master 2026-07-23 16:00:24 +00:00
21 changed files with 191 additions and 113 deletions
Showing only changes of commit 89c1bfe08f - Show all commits

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ build/
dist/ dist/
wheels/ wheels/
*.egg-info *.egg-info
.mypy_cache
# Virtual environments # Virtual environments
.venv .venv

5
pre-commit Normal file
View File

@ -0,0 +1,5 @@
set -o pipefail
uv run mypy src/
uv run ruff check "$@"
git diff --exit-code

View File

@ -17,3 +17,31 @@ build-backend = "uv_build"
dev = [ dev = [
"ruff>=0.15.1", "ruff>=0.15.1",
] ]
[tool.mypy]
disable_error_code = ["import-untyped"]
[tool.ruff.lint]
select = [
"E", # standard style checks
"F", # standard code checks
"FAST", # for fastAPI
"ANN", # for annotations
"B", # for common bug causes
"A", # for builtin variable shadowing
"PIE", # for unnecessary code
"SIM", # for simplifications
"TID", # for tidy improts
"TC" , # for type checking
#"ARG", # for unused arguments
#"TD", # checks for TODOs
"N", # naming conventions
"W", # whitespace warnings (use --fix)
"PLC", # common conventions
"PLE", # common errors
"PLR", # common refactors
"PLW", # common warnings
"UP", # for old style
"RUF", # other RUF specific rules
]
extend-ignore = ["TC", "PLR2004"]

View File

@ -0,0 +1,7 @@
from pydantic import BaseModel, UUID7
from .refs import CollectionName
class FiberBaseModel(BaseModel):
uuid: UUID7
collection_name: CollectionName

View File

@ -0,0 +1,58 @@
from pydantic import model_validator, UUID7
from fiber_package.refs import (
CommitteeEditionRef,
CommitteeEditionRefList,
EntityRefList
)
from fiber_package.base import FiberBaseModel
from typing import Self, Literal
from datetime import date
from .enums import CommitteeType
'''
Committees are groups of members for a definite period.
Every committee has a list of editions.
An edition is a definite group of members with functions and a timespan.
'''
class CommitteeModel(FiberBaseModel):
collection_name: Literal["committee"] = "committee"
full_name_en: str
full_name_nl: str
description_en: str = ""
description_nl: str = ""
committee_type: CommitteeType
active: bool = True
editions: CommitteeEditionRefList
current_edition: CommitteeEditionRef | None
@model_validator(mode='after')
def current_edition_in_editions(self) -> Self:
if self.current_edition is None:
return self
uuid = self.current_edition.uuid
if uuid not in self.editions:
raise ValueError("Current edition must be in list of editions")
return self
class CommitteeEditionModel(FiberBaseModel):
collection_name: Literal["committee_edition"] = "committee_edition"
members: EntityRefList
functions: dict[UUID7, str]
start_date: date
end_date: date | None # None means ongoing
@model_validator(mode='after')
def functions_to_members_only(self) -> Self:
for uuid in self.functions:
if uuid not in self.members:
raise ValueError("Cannot only assign functions to committee members")
return self

View File

@ -1,6 +0,0 @@
from pydantic import BaseModel
from .submodels import LoginInfo, LoginInfoResponse
class LoginModel(BaseModel):
login: LoginInfo | LoginInfoResponse

View File

@ -15,7 +15,7 @@ class Gender(StrEnum):
nonbinary = "n" nonbinary = "n"
class HonoraryStatus(StrEnum): class HonoraryStatus(StrEnum):
merit = ("merit",) merit = "merit"
honorary = "honorary" honorary = "honorary"
class OnlineAlmanacField(StrEnum): class OnlineAlmanacField(StrEnum):

View File

@ -1,13 +1,12 @@
from typing import Literal
from pydantic import Field from pydantic import Field
from .non_members import FormerMemberModel from .non_members import FormerMemberModel
from .enums import Membership, OnlineAlmanacField from .enums import OnlineAlmanacField
from .submodels import Address, LoginInfo from .submodels import Address, LoginInfo
class MemberModel(FormerMemberModel): class MemberModel(FormerMemberModel):
# requires that Membership equals member # requires that Membership equals member
membership: Literal[Membership.member] = Membership.member # membership: Literal[Membership.member] = Membership.member
login: LoginInfo login: LoginInfo
address: Address address: Address

View File

@ -1,8 +1,6 @@
from typing import Literal
from pydantic import Field
from .person import PersonModel from .person import PersonModel
from .enums import HonoraryStatus, Membership, Gender from .enums import HonoraryStatus, Gender
class FormerMemberModel(PersonModel): class FormerMemberModel(PersonModel):
title: str | None = None title: str | None = None
@ -13,12 +11,13 @@ class FormerMemberModel(PersonModel):
# company emails? # company emails?
class AlumnModel(FormerMemberModel): class AlumnModel(FormerMemberModel):
membership: Literal[Membership.alumn] = Membership.alumn # membership: Literal[Membership.alumn] = Membership.alumn
graduated: bool = False graduated: bool = False
receive_spam_mail: bool = True receive_spam_mail: bool = True
class ExternalModel(PersonModel): class ExternalModel(PersonModel):
membership: Literal[Membership.extern] = Membership.extern # membership: Literal[Membership.extern] = Membership.extern
pass
# TODO... payment??? # TODO... payment???
# TODO require login info?? # TODO require login info??

View File

@ -1,10 +1,13 @@
from pydantic import BaseModel, model_validator, EmailStr from pydantic import model_validator, EmailStr
from typing import Self from typing import Self, Literal
from fiber_package.base import FiberBaseModel
from .enums import Membership, LanguageOption from .enums import Membership, LanguageOption
from .submodels import Address from .submodels import Address
class PersonModel(BaseModel): class PersonModel(FiberBaseModel):
collection_name: Literal["entity"] = "entity"
membership: Membership membership: Membership
first_name: str first_name: str
tussenvoegsels: str | None = None # only in Dutch names tussenvoegsels: str | None = None # only in Dutch names

View File

@ -0,0 +1,7 @@
from .submodels import LoginInfo, LoginInfoResponse
from fiber_package.base import FiberBaseModel
class LoginModel(FiberBaseModel):
login: LoginInfo | LoginInfoResponse

View File

@ -1,12 +0,0 @@
from pydantic import BaseModel
from fiber_package.util import EntityList, GroupList
class AssociationModel(BaseModel):
full_name: str
members: EntityList
committees: GroupList
guilds: GroupList
# events: GroupList
# activities: GroupList
# mailings: GroupList

View File

@ -1,50 +0,0 @@
from pydantic import BaseModel, model_validator
from fiber_package.util import EntityList, GroupList, GroupRef
from typing import Self
from datetime import date
from .enums import CommitteeType
'''
Committees are groups of members for a definite period.
Every committee has a list of editions.
An edition is a definite group of members with functions and a timespan.
'''
class CommitteeModel(BaseModel):
full_name_EN: str
full_name_NL: str
description_EN: str = ""
description_NL: str = ""
committee_type: CommitteeType
active: bool = True
editions: GroupList
current_edition: GroupRef | None
@model_validator(mode='after')
def current_edition_in_editions(self) -> Self:
uuid = self.current_edition
if uuid is None:
return self
if uuid not in self.editions:
raise ValueError("Current edition must be in list of editions")
return self
# TODO... maybe require that an active board always has a current_edition that is non-empty?
class CommitteeEditionModel(BaseModel):
members: EntityList
functions: dict[UUID4, str]
start_date: date
end_date: date | None # None means ongoing
@model_validator(mode='after')
def functions_to_members_only(self) -> Self:
for uuid in self.functions.keys():
if uuid not in self.members:
raise ValueError("Cannot only assign functions to members")
return self

View File

@ -1,6 +0,0 @@
from pydantic import BaseModel
from fiber_package.util import GroupList, EntityList
class RootGroupModel(BaseModel):
associations: GroupList
sys_admins: EntityList

View File

@ -1,9 +1,11 @@
from pydantic import BaseModel, model_validator from pydantic import model_validator
from fiber_package.util import EntityList from fiber_package.refs import EntityRefList, EntityRef
from datetime import date from datetime import date
from typing import Self from typing import Self, Literal
from fiber_package.base import FiberBaseModel
''' '''
@ -13,16 +15,18 @@ They have a list of members.
Removing old members from last_active is done in fiber Removing old members from last_active is done in fiber
''' '''
class GuildModel(BaseModel): class GuildModel(FiberBaseModel):
full_name_EN: str collection_name: Literal["guild"] = "guild"
full_name_NL: str
description_EN: str
description_NL: str
members: EntityList full_name_en: str
previous_members: EntityList full_name_nl: str
description_en: str
description_nl: str
guild_masters: EntityList members: EntityRefList
previous_members: EntityRefList
guild_masters: EntityRefList
last_active: dict[EntityRef, date] last_active: dict[EntityRef, date]
@ -30,7 +34,9 @@ class GuildModel(BaseModel):
def current_members_not_previous(self) -> Self: def current_members_not_previous(self) -> Self:
for member in self.members: for member in self.members:
if member in self.previous_members: if member in self.previous_members:
raise ValueError("There can be no overlap between members and previous members!") raise ValueError(
"There can be no overlap between members and previous members!"
)
return self return self
@model_validator(mode='after') @model_validator(mode='after')
@ -39,4 +45,3 @@ class GuildModel(BaseModel):
if master not in self.members: if master not in self.members:
raise ValueError("All guild masters must be members!") raise ValueError("All guild masters must be members!")
return self return self

View File

@ -1,23 +1,42 @@
from dataclasses import dataclass from dataclasses import dataclass
from pydantic import UUID7 from pydantic import UUID7
from typing import Literal from typing import Literal, TypeVar
from collections.abc import Iterator
'''
CollectionName provides the list of all allowed collections!
'''
CollectionName = Literal["system", "entity", "committee", "committee_edition", "guild"]
C = TypeVar("C", bound=str)
@dataclass(slots=True) @dataclass(slots=True)
class SystemRef(Ref): class Ref[C: str]:
uuid: UUID7 uuid: UUID7
collection: Literal["system"] = "system" collection_name: C
class CommitteeRef(Ref): @dataclass(slots=True)
uuid: UUID7 class RefList[C: str]:
collection: Literal["committee"] = "committee" uuids: list[UUID7]
collection_name: C
Ref = SystemRef | CommitteeRef def __iter__(self) -> Iterator[UUID7]:
return iter(self.uuids)
# TODO.... def __contains__(self, item: UUID7) -> bool:
return item in self.uuids
SystemRef = Ref[Literal["system"]]
SystemRefList = RefList[Literal["system"]]
EntityList = Annotated[list[UUID4], Field(default_factory=list)] EntityRef = Ref[Literal["entity"]]
GroupList = Annotated[list[UUID4], Field(default_factory=list)] EntityRefList = RefList[Literal["entity"]]
EntityRef = UUID4 CommitteeRef = Ref[Literal["committee"]]
GroupRef = UUID4 CommitteeRefList = RefList[Literal["committee"]]
CommitteeEditionRef = Ref[Literal["committee_edition"]]
CommitteeEditionRefList = RefList[Literal["committee_edition"]]
GuildRef = Ref[Literal["guild"]]
GuildRefList = RefList[Literal["guild"]]

View File

@ -0,0 +1,15 @@
from fiber_package.base import FiberBaseModel
from fiber_package.refs import EntityRefList, CommitteeRefList, GuildRefList
class AssociationModel(FiberBaseModel):
full_name: str
members: EntityRefList
committees: CommitteeRefList
guilds: GuildRefList
# TODO: also add former_members, alumns, etc... ?
# events: GroupList
# activities: GroupList
# mailings: GroupList

View File

@ -0,0 +1,6 @@
from fiber_package.base import FiberBaseModel
from fiber_package.refs import SystemRefList, EntityRefList
class RootGroupModel(FiberBaseModel):
associations: SystemRefList
sys_admins: EntityRefList