Compare commits

..

8 Commits

21 changed files with 178 additions and 83 deletions

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

@ -1,7 +1,13 @@
from pydantic import BaseModel, model_validator from pydantic import model_validator, UUID7
from fiber_package.util import EntityList, GroupList, GroupRef
from typing import Self from fiber_package.refs import (
CommitteeEditionRef,
CommitteeEditionRefList,
EntityRefList
)
from fiber_package.base import FiberBaseModel
from typing import Self, Literal
from datetime import date from datetime import date
from .enums import CommitteeType from .enums import CommitteeType
@ -12,31 +18,33 @@ Every committee has a list of editions.
An edition is a definite group of members with functions and a timespan. An edition is a definite group of members with functions and a timespan.
''' '''
class CommitteeModel(BaseModel): class CommitteeModel(FiberBaseModel):
full_name_EN: str collection_name: Literal["committee"] = "committee"
full_name_NL: str
description_EN: str = "" full_name_en: str
description_NL: str = "" full_name_nl: str
description_en: str = ""
description_nl: str = ""
committee_type: CommitteeType committee_type: CommitteeType
active: bool = True active: bool = True
editions: GroupList editions: CommitteeEditionRefList
current_edition: GroupRef | None current_edition: CommitteeEditionRef | None
@model_validator(mode='after') @model_validator(mode='after')
def current_edition_in_editions(self) -> Self: def current_edition_in_editions(self) -> Self:
uuid = self.current_edition if self.current_edition is None:
if uuid is None:
return self return self
uuid = self.current_edition.uuid
if uuid not in self.editions: if uuid not in self.editions:
raise ValueError("Current edition must be in list of editions") raise ValueError("Current edition must be in list of editions")
return self return self
# TODO... maybe require that an active board always has a current_edition that is non-empty? class CommitteeEditionModel(FiberBaseModel):
collection_name: Literal["committee_edition"] = "committee_edition"
class CommitteeEditionModel(BaseModel): members: EntityRefList
members: EntityList
functions: dict[UUID7, str] functions: dict[UUID7, str]
start_date: date start_date: date
@ -44,7 +52,7 @@ class CommitteeEditionModel(BaseModel):
@model_validator(mode='after') @model_validator(mode='after')
def functions_to_members_only(self) -> Self: def functions_to_members_only(self) -> Self:
for uuid in self.functions.keys(): for uuid in self.functions:
if uuid not in self.members: if uuid not in self.members:
raise ValueError("Cannot only assign functions to members") raise ValueError("Cannot only assign functions to committee members")
return self 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

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

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
@ -23,4 +22,4 @@ class MemberModel(FormerMemberModel):
online_almanac_fields: list[OnlineAlmanacField] = Field(default_factory=list) online_almanac_fields: list[OnlineAlmanacField] = Field(default_factory=list)
# IT stuff # IT stuff
shell: str = "/bin/bash" # TODO: support other shells? shell: str = "/bin/bash" # TODO: support other shells?
mail_token: str | None = None mail_token: str | None = None

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

@ -1,17 +1,20 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import date from datetime import date
# always use @dataclass with slots=True for better performance from pydantic import BaseModel
@dataclass(slots=True)
class LoginInfo:
# always use @dataclass with slots=True for better performance
# unless if you have unions like LoginInfo and Login...
class LoginInfo(BaseModel):
username: str username: str
hashed_password: str hashed_password: str
refresh_tokens: list[str] = field(default_factory=list) refresh_tokens: list[str] = field(default_factory=list)
disabled: bool = False disabled: bool = False
@dataclass(slots=True) class LoginInfoResponse(BaseModel):
class LoginInfoResponse:
username: str username: str
@dataclass(slots=True) @dataclass(slots=True)
@ -29,4 +32,4 @@ class PublicKey:
name: str name: str
key_type: str key_type: str
key: str key: str
expires: date expires: date

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,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

42
src/fiber_package/refs.py Normal file
View File

@ -0,0 +1,42 @@
from dataclasses import dataclass
from pydantic import UUID7
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)
class Ref[C: str]:
uuid: UUID7
collection_name: C
@dataclass(slots=True)
class RefList[C: str]:
uuids: list[UUID7]
collection_name: C
def __iter__(self) -> Iterator[UUID7]:
return iter(self.uuids)
def __contains__(self, item: UUID7) -> bool:
return item in self.uuids
SystemRef = Ref[Literal["system"]]
SystemRefList = RefList[Literal["system"]]
EntityRef = Ref[Literal["entity"]]
EntityRefList = RefList[Literal["entity"]]
CommitteeRef = Ref[Literal["committee"]]
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

View File

@ -1,9 +0,0 @@
from typing import Annotated
from pydantic import RootModel, Field, UUID7
EntityList = Annotated[list[UUID7], Field(default_factory=list)]
GroupList = Annotated[list[UUID7], Field(default_factory=list)]
EntityRef = UUID7
GroupRef = UUID7