diff --git a/.gitignore b/.gitignore index 505a3b1..f1e7339 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/ dist/ wheels/ *.egg-info +.mypy_cache # Virtual environments .venv diff --git a/pre-commit b/pre-commit new file mode 100644 index 0000000..f7b3d81 --- /dev/null +++ b/pre-commit @@ -0,0 +1,5 @@ +set -o pipefail + +uv run mypy src/ +uv run ruff check "$@" +git diff --exit-code diff --git a/pyproject.toml b/pyproject.toml index 223c779..a9f0e33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] \ No newline at end of file diff --git a/src/fiber_package/base.py b/src/fiber_package/base.py new file mode 100644 index 0000000..26dfda4 --- /dev/null +++ b/src/fiber_package/base.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, UUID7 + +from .refs import CollectionName + +class FiberBaseModel(BaseModel): + uuid: UUID7 + collection_name: CollectionName diff --git a/src/fiber_package/groups/committees/committee.py b/src/fiber_package/committee/committee.py similarity index 50% rename from src/fiber_package/groups/committees/committee.py rename to src/fiber_package/committee/committee.py index 898da4f..4b57b21 100644 --- a/src/fiber_package/groups/committees/committee.py +++ b/src/fiber_package/committee/committee.py @@ -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 \ No newline at end of file + raise ValueError("Cannot only assign functions to committee members") + return self diff --git a/src/fiber_package/groups/committees/enums.py b/src/fiber_package/committee/enums.py similarity index 100% rename from src/fiber_package/groups/committees/enums.py rename to src/fiber_package/committee/enums.py diff --git a/src/fiber_package/entities/system.py b/src/fiber_package/entities/system.py deleted file mode 100644 index 80276af..0000000 --- a/src/fiber_package/entities/system.py +++ /dev/null @@ -1,6 +0,0 @@ -from pydantic import BaseModel - -from .submodels import LoginInfo, LoginInfoResponse - -class LoginModel(BaseModel): - login: LoginInfo | LoginInfoResponse \ No newline at end of file diff --git a/src/fiber_package/entities/__init__.py b/src/fiber_package/entity/__init__.py similarity index 100% rename from src/fiber_package/entities/__init__.py rename to src/fiber_package/entity/__init__.py diff --git a/src/fiber_package/entities/enums.py b/src/fiber_package/entity/enums.py similarity index 95% rename from src/fiber_package/entities/enums.py rename to src/fiber_package/entity/enums.py index 3d5ccc1..ba27d87 100644 --- a/src/fiber_package/entities/enums.py +++ b/src/fiber_package/entity/enums.py @@ -15,7 +15,7 @@ class Gender(StrEnum): nonbinary = "n" class HonoraryStatus(StrEnum): - merit = ("merit",) + merit = "merit" honorary = "honorary" class OnlineAlmanacField(StrEnum): diff --git a/src/fiber_package/entity/login.py b/src/fiber_package/entity/login.py new file mode 100644 index 0000000..f68b07a --- /dev/null +++ b/src/fiber_package/entity/login.py @@ -0,0 +1,7 @@ + +from .submodels import LoginInfo, LoginInfoResponse + +from fiber_package.base import FiberBaseModel + +class LoginModel(FiberBaseModel): + login: LoginInfo | LoginInfoResponse diff --git a/src/fiber_package/entities/member.py b/src/fiber_package/entity/member.py similarity index 79% rename from src/fiber_package/entities/member.py rename to src/fiber_package/entity/member.py index 3499199..62ba032 100644 --- a/src/fiber_package/entities/member.py +++ b/src/fiber_package/entity/member.py @@ -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 \ No newline at end of file + mail_token: str | None = None diff --git a/src/fiber_package/entities/non_members.py b/src/fiber_package/entity/non_members.py similarity index 66% rename from src/fiber_package/entities/non_members.py rename to src/fiber_package/entity/non_members.py index 7eb3856..d3a53ab 100644 --- a/src/fiber_package/entities/non_members.py +++ b/src/fiber_package/entity/non_members.py @@ -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?? diff --git a/src/fiber_package/entities/person.py b/src/fiber_package/entity/person.py similarity index 76% rename from src/fiber_package/entities/person.py rename to src/fiber_package/entity/person.py index 0befc52..d8f4a2c 100644 --- a/src/fiber_package/entities/person.py +++ b/src/fiber_package/entity/person.py @@ -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 diff --git a/src/fiber_package/entities/submodels.py b/src/fiber_package/entity/submodels.py similarity index 75% rename from src/fiber_package/entities/submodels.py rename to src/fiber_package/entity/submodels.py index 37e52fb..8aefb84 100644 --- a/src/fiber_package/entities/submodels.py +++ b/src/fiber_package/entity/submodels.py @@ -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 \ No newline at end of file + expires: date diff --git a/src/fiber_package/groups/association.py b/src/fiber_package/groups/association.py deleted file mode 100644 index d5fcea7..0000000 --- a/src/fiber_package/groups/association.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/fiber_package/groups/root.py b/src/fiber_package/groups/root.py deleted file mode 100644 index 2d3bfbd..0000000 --- a/src/fiber_package/groups/root.py +++ /dev/null @@ -1,6 +0,0 @@ -from pydantic import BaseModel -from fiber_package.util import GroupList, EntityList - -class RootGroupModel(BaseModel): - associations: GroupList - sys_admins: EntityList \ No newline at end of file diff --git a/src/fiber_package/groups/committees/guild.py b/src/fiber_package/guild/guild.py similarity index 53% rename from src/fiber_package/groups/committees/guild.py rename to src/fiber_package/guild/guild.py index e21e107..03e2822 100644 --- a/src/fiber_package/groups/committees/guild.py +++ b/src/fiber_package/guild/guild.py @@ -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 - \ No newline at end of file diff --git a/src/fiber_package/refs.py b/src/fiber_package/refs.py new file mode 100644 index 0000000..3088c69 --- /dev/null +++ b/src/fiber_package/refs.py @@ -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"]] diff --git a/src/fiber_package/system/association.py b/src/fiber_package/system/association.py new file mode 100644 index 0000000..857fcdf --- /dev/null +++ b/src/fiber_package/system/association.py @@ -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 diff --git a/src/fiber_package/system/root.py b/src/fiber_package/system/root.py new file mode 100644 index 0000000..7b8bd6c --- /dev/null +++ b/src/fiber_package/system/root.py @@ -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 diff --git a/src/fiber_package/util.py b/src/fiber_package/util.py deleted file mode 100644 index f4b84c4..0000000 --- a/src/fiber_package/util.py +++ /dev/null @@ -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 \ No newline at end of file