Compare commits

...

8 Commits

21 changed files with 178 additions and 83 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@ build/
dist/
wheels/
*.egg-info
.mypy_cache
# Virtual environments
.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 = [
"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 fiber_package.util import EntityList, GroupList, GroupRef
from pydantic import model_validator, UUID7
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 .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.
'''
class CommitteeModel(BaseModel):
full_name_EN: str
full_name_NL: str
description_EN: str = ""
description_NL: str = ""
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: GroupList
current_edition: GroupRef | None
editions: CommitteeEditionRefList
current_edition: CommitteeEditionRef | None
@model_validator(mode='after')
def current_edition_in_editions(self) -> Self:
uuid = self.current_edition
if uuid is None:
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
# 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: EntityList
members: EntityRefList
functions: dict[UUID7, str]
start_date: date
@ -44,7 +52,7 @@ class CommitteeEditionModel(BaseModel):
@model_validator(mode='after')
def functions_to_members_only(self) -> Self:
for uuid in self.functions.keys():
for uuid in self.functions:
if uuid not in self.members:
raise ValueError("Cannot only assign functions to members")
return self
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"
class HonoraryStatus(StrEnum):
merit = ("merit",)
merit = "merit"
honorary = "honorary"
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 .non_members import FormerMemberModel
from .enums import Membership, OnlineAlmanacField
from .enums import OnlineAlmanacField
from .submodels import Address, LoginInfo
class MemberModel(FormerMemberModel):
# requires that Membership equals member
membership: Literal[Membership.member] = Membership.member
# membership: Literal[Membership.member] = Membership.member
login: LoginInfo
address: Address
@ -23,4 +22,4 @@ class MemberModel(FormerMemberModel):
online_almanac_fields: list[OnlineAlmanacField] = Field(default_factory=list)
# IT stuff
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 .enums import HonoraryStatus, Membership, Gender
from .enums import HonoraryStatus, Gender
class FormerMemberModel(PersonModel):
title: str | None = None
@ -13,12 +11,13 @@ class FormerMemberModel(PersonModel):
# company emails?
class AlumnModel(FormerMemberModel):
membership: Literal[Membership.alumn] = Membership.alumn
# membership: Literal[Membership.alumn] = Membership.alumn
graduated: bool = False
receive_spam_mail: bool = True
class ExternalModel(PersonModel):
membership: Literal[Membership.extern] = Membership.extern
# membership: Literal[Membership.extern] = Membership.extern
pass
# TODO... payment???
# TODO require login info??

View File

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

View File

@ -1,17 +1,20 @@
from dataclasses import dataclass, field
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
hashed_password: str
refresh_tokens: list[str] = field(default_factory=list)
disabled: bool = False
@dataclass(slots=True)
class LoginInfoResponse:
class LoginInfoResponse(BaseModel):
username: str
@dataclass(slots=True)
@ -29,4 +32,4 @@ class PublicKey:
name: str
key_type: 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 fiber_package.util import EntityList
from pydantic import model_validator
from fiber_package.refs import EntityRefList, EntityRef
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
'''
class GuildModel(BaseModel):
full_name_EN: str
full_name_NL: str
description_EN: str
description_NL: str
class GuildModel(FiberBaseModel):
collection_name: Literal["guild"] = "guild"
members: EntityList
previous_members: EntityList
full_name_en: str
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]
@ -30,7 +34,9 @@ class GuildModel(BaseModel):
def current_members_not_previous(self) -> Self:
for member in self.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
@model_validator(mode='after')
@ -39,4 +45,3 @@ class GuildModel(BaseModel):
if master not in self.members:
raise ValueError("All guild masters must be members!")
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