Add ruff linter/formatter for Python
- Add ruff as dev dependency - Configure ruff in pyproject.toml with strict 88-char line limit - Ignore B008 (FastAPI Depends pattern is standard) - Allow longer lines in tests for readability - Fix all lint issues in source files - Add Makefile targets: lint-backend, format-backend, fix-backend
This commit is contained in:
parent
69bc8413e0
commit
6c218130e9
31 changed files with 1234 additions and 876 deletions
|
|
@ -1,9 +1,24 @@
|
|||
from datetime import datetime, date, time, timezone
|
||||
from datetime import UTC, date, datetime, time
|
||||
from enum import Enum as PyEnum
|
||||
from typing import TypedDict
|
||||
from sqlalchemy import Integer, String, Float, DateTime, Date, Time, ForeignKey, Table, Column, Enum, UniqueConstraint, select
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
Date,
|
||||
DateTime,
|
||||
Enum,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
String,
|
||||
Table,
|
||||
Time,
|
||||
UniqueConstraint,
|
||||
select,
|
||||
)
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from database import Base
|
||||
|
||||
|
||||
|
|
@ -14,25 +29,26 @@ class RoleConfig(TypedDict):
|
|||
|
||||
class Permission(str, PyEnum):
|
||||
"""All available permissions in the system."""
|
||||
|
||||
# Counter permissions
|
||||
VIEW_COUNTER = "view_counter"
|
||||
INCREMENT_COUNTER = "increment_counter"
|
||||
|
||||
|
||||
# Sum permissions
|
||||
USE_SUM = "use_sum"
|
||||
|
||||
|
||||
# Audit permissions
|
||||
VIEW_AUDIT = "view_audit"
|
||||
|
||||
|
||||
# Invite permissions
|
||||
MANAGE_INVITES = "manage_invites"
|
||||
VIEW_OWN_INVITES = "view_own_invites"
|
||||
|
||||
|
||||
# Booking permissions (regular users)
|
||||
BOOK_APPOINTMENT = "book_appointment"
|
||||
VIEW_OWN_APPOINTMENTS = "view_own_appointments"
|
||||
CANCEL_OWN_APPOINTMENT = "cancel_own_appointment"
|
||||
|
||||
|
||||
# Availability/Appointments permissions (admin)
|
||||
MANAGE_AVAILABILITY = "manage_availability"
|
||||
VIEW_ALL_APPOINTMENTS = "view_all_appointments"
|
||||
|
|
@ -41,6 +57,7 @@ class Permission(str, PyEnum):
|
|||
|
||||
class InviteStatus(str, PyEnum):
|
||||
"""Status of an invite."""
|
||||
|
||||
READY = "ready"
|
||||
SPENT = "spent"
|
||||
REVOKED = "revoked"
|
||||
|
|
@ -48,6 +65,7 @@ class InviteStatus(str, PyEnum):
|
|||
|
||||
class AppointmentStatus(str, PyEnum):
|
||||
"""Status of an appointment."""
|
||||
|
||||
BOOKED = "booked"
|
||||
CANCELLED_BY_USER = "cancelled_by_user"
|
||||
CANCELLED_BY_ADMIN = "cancelled_by_admin"
|
||||
|
|
@ -60,7 +78,7 @@ ROLE_REGULAR = "regular"
|
|||
# Role definitions with their permissions
|
||||
ROLE_DEFINITIONS: dict[str, RoleConfig] = {
|
||||
ROLE_ADMIN: {
|
||||
"description": "Administrator with audit, invite, and appointment management access",
|
||||
"description": "Administrator with audit/invite/appointment access",
|
||||
"permissions": [
|
||||
Permission.VIEW_AUDIT,
|
||||
Permission.MANAGE_INVITES,
|
||||
|
|
@ -88,7 +106,12 @@ ROLE_DEFINITIONS: dict[str, RoleConfig] = {
|
|||
role_permissions = Table(
|
||||
"role_permissions",
|
||||
Base.metadata,
|
||||
Column("role_id", Integer, ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True),
|
||||
Column(
|
||||
"role_id",
|
||||
Integer,
|
||||
ForeignKey("roles.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
Column("permission", Enum(Permission), primary_key=True),
|
||||
)
|
||||
|
||||
|
|
@ -97,8 +120,18 @@ role_permissions = Table(
|
|||
user_roles = Table(
|
||||
"user_roles",
|
||||
Base.metadata,
|
||||
Column("user_id", Integer, ForeignKey("users.id", ondelete="CASCADE"), primary_key=True),
|
||||
Column("role_id", Integer, ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True),
|
||||
Column(
|
||||
"user_id",
|
||||
Integer,
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
Column(
|
||||
"role_id",
|
||||
Integer,
|
||||
ForeignKey("roles.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -108,7 +141,7 @@ class Role(Base):
|
|||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
|
||||
description: Mapped[str] = mapped_column(String(255), nullable=True)
|
||||
|
||||
|
||||
# Relationship to users
|
||||
users: Mapped[list["User"]] = relationship(
|
||||
"User",
|
||||
|
|
@ -118,31 +151,42 @@ class Role(Base):
|
|||
|
||||
async def get_permissions(self, db: AsyncSession) -> set[Permission]:
|
||||
"""Get all permissions for this role."""
|
||||
result = await db.execute(
|
||||
select(role_permissions.c.permission).where(role_permissions.c.role_id == self.id)
|
||||
query = select(role_permissions.c.permission).where(
|
||||
role_permissions.c.role_id == self.id
|
||||
)
|
||||
result = await db.execute(query)
|
||||
return {row[0] for row in result.fetchall()}
|
||||
|
||||
async def set_permissions(self, db: AsyncSession, permissions: list[Permission]) -> None:
|
||||
async def set_permissions(
|
||||
self, db: AsyncSession, permissions: list[Permission]
|
||||
) -> None:
|
||||
"""Set all permissions for this role (replaces existing)."""
|
||||
await db.execute(role_permissions.delete().where(role_permissions.c.role_id == self.id))
|
||||
delete_query = role_permissions.delete().where(
|
||||
role_permissions.c.role_id == self.id
|
||||
)
|
||||
await db.execute(delete_query)
|
||||
for perm in permissions:
|
||||
await db.execute(role_permissions.insert().values(role_id=self.id, permission=perm))
|
||||
insert_query = role_permissions.insert().values(
|
||||
role_id=self.id, permission=perm
|
||||
)
|
||||
await db.execute(insert_query)
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
email: Mapped[str] = mapped_column(
|
||||
String(255), unique=True, nullable=False, index=True
|
||||
)
|
||||
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
|
||||
|
||||
# Contact details (all optional)
|
||||
contact_email: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
telegram: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
signal: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
nostr_npub: Mapped[str | None] = mapped_column(String(63), nullable=True)
|
||||
|
||||
|
||||
# Godfather (who invited this user) - null for seeded/admin users
|
||||
godfather_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=True
|
||||
|
|
@ -152,7 +196,7 @@ class User(Base):
|
|||
remote_side="User.id",
|
||||
foreign_keys=[godfather_id],
|
||||
)
|
||||
|
||||
|
||||
# Relationship to roles
|
||||
roles: Mapped[list[Role]] = relationship(
|
||||
"Role",
|
||||
|
|
@ -192,12 +236,14 @@ class SumRecord(Base):
|
|||
__tablename__ = "sum_records"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=False, index=True
|
||||
)
|
||||
a: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
b: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
result: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -205,11 +251,13 @@ class CounterRecord(Base):
|
|||
__tablename__ = "counter_records"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=False, index=True
|
||||
)
|
||||
value_before: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
value_after: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -217,11 +265,13 @@ class Invite(Base):
|
|||
__tablename__ = "invites"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
identifier: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
|
||||
identifier: Mapped[str] = mapped_column(
|
||||
String(64), unique=True, nullable=False, index=True
|
||||
)
|
||||
status: Mapped[InviteStatus] = mapped_column(
|
||||
Enum(InviteStatus), nullable=False, default=InviteStatus.READY
|
||||
)
|
||||
|
||||
|
||||
# Godfather - the user who owns this invite
|
||||
godfather_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=False, index=True
|
||||
|
|
@ -231,7 +281,7 @@ class Invite(Base):
|
|||
foreign_keys=[godfather_id],
|
||||
lazy="joined",
|
||||
)
|
||||
|
||||
|
||||
# User who used this invite (null until spent)
|
||||
used_by_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=True
|
||||
|
|
@ -241,17 +291,22 @@ class Invite(Base):
|
|||
foreign_keys=[used_by_id],
|
||||
lazy="joined",
|
||||
)
|
||||
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC)
|
||||
)
|
||||
spent_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
spent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
class Availability(Base):
|
||||
"""Admin availability slots for booking."""
|
||||
|
||||
__tablename__ = "availability"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("date", "start_time", name="uq_availability_date_start"),
|
||||
|
|
@ -262,34 +317,37 @@ class Availability(Base):
|
|||
start_time: Mapped[time] = mapped_column(Time, nullable=False)
|
||||
end_time: Mapped[time] = mapped_column(Time, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC)
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc)
|
||||
DateTime(timezone=True),
|
||||
default=lambda: datetime.now(UTC),
|
||||
onupdate=lambda: datetime.now(UTC),
|
||||
)
|
||||
|
||||
|
||||
class Appointment(Base):
|
||||
"""User appointment bookings."""
|
||||
|
||||
__tablename__ = "appointments"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("slot_start", name="uq_appointment_slot_start"),
|
||||
)
|
||||
__table_args__ = (UniqueConstraint("slot_start", name="uq_appointment_slot_start"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("users.id"), nullable=False, index=True
|
||||
)
|
||||
user: Mapped[User] = relationship("User", foreign_keys=[user_id], lazy="joined")
|
||||
slot_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
slot_start: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, index=True
|
||||
)
|
||||
slot_end: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
note: Mapped[str | None] = mapped_column(String(144), nullable=True)
|
||||
status: Mapped[AppointmentStatus] = mapped_column(
|
||||
Enum(AppointmentStatus), nullable=False, default=AppointmentStatus.BOOKED
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||
DateTime(timezone=True), default=lambda: datetime.now(UTC)
|
||||
)
|
||||
cancelled_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
cancelled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue