Fixing build
This commit is contained in:
@@ -53,6 +53,9 @@ target-version = "py312"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP", "B", "SIM"]
|
||||
ignore = ["B008", "B904", "UP046", "E501", "SIM102", "SIM211", "F841"]
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = ["F401", "F841", "SIM102", "SIM211", "UP017", "I001", "B017"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.12"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from functools import lru_cache
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
@@ -35,10 +34,10 @@ class Member(Base):
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False, index=True)
|
||||
display_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
avatar_url: Mapped[Optional[str]] = mapped_column(Text)
|
||||
nc_username: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
nc_url: Mapped[Optional[str]] = mapped_column(Text)
|
||||
nc_password: Mapped[Optional[str]] = mapped_column(Text)
|
||||
avatar_url: Mapped[str | None] = mapped_column(Text)
|
||||
nc_username: Mapped[str | None] = mapped_column(String(255))
|
||||
nc_url: Mapped[str | None] = mapped_column(Text)
|
||||
nc_password: Mapped[str | None] = mapped_column(Text)
|
||||
password_hash: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
@@ -68,8 +67,8 @@ class Band(Base):
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
nc_folder_path: Mapped[Optional[str]] = mapped_column(Text)
|
||||
nc_user: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
nc_folder_path: Mapped[str | None] = mapped_column(Text)
|
||||
nc_user: Mapped[str | None] = mapped_column(String(255))
|
||||
genre_tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
@@ -103,7 +102,7 @@ class BandMember(Base):
|
||||
joined_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
instrument: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
instrument: Mapped[str | None] = mapped_column(String(100))
|
||||
|
||||
band: Mapped[Band] = relationship("Band", back_populates="memberships")
|
||||
member: Mapped[Member] = relationship("Member", back_populates="band_memberships")
|
||||
@@ -122,8 +121,8 @@ class BandInvite(Base):
|
||||
UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
used_by: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
used_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL")
|
||||
)
|
||||
|
||||
@@ -143,9 +142,9 @@ class RehearsalSession(Base):
|
||||
UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
date: Mapped[datetime] = mapped_column(DateTime(timezone=False), nullable=False)
|
||||
nc_folder_path: Mapped[Optional[str]] = mapped_column(Text)
|
||||
label: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
notes: Mapped[Optional[str]] = mapped_column(Text)
|
||||
nc_folder_path: Mapped[str | None] = mapped_column(Text)
|
||||
label: Mapped[str | None] = mapped_column(String(255))
|
||||
notes: Mapped[str | None] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
@@ -164,17 +163,17 @@ class Song(Base):
|
||||
band_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
session_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
session_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("rehearsal_sessions.id", ondelete="SET NULL"), index=True
|
||||
)
|
||||
title: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
nc_folder_path: Mapped[Optional[str]] = mapped_column(Text)
|
||||
nc_folder_path: Mapped[str | None] = mapped_column(Text)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="jam")
|
||||
tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False)
|
||||
global_key: Mapped[Optional[str]] = mapped_column(String(30))
|
||||
global_bpm: Mapped[Optional[float]] = mapped_column(Numeric(6, 2))
|
||||
notes: Mapped[Optional[str]] = mapped_column(Text)
|
||||
created_by: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
global_key: Mapped[str | None] = mapped_column(String(30))
|
||||
global_bpm: Mapped[float | None] = mapped_column(Numeric(6, 2))
|
||||
notes: Mapped[str | None] = mapped_column(Text)
|
||||
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL")
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
@@ -185,8 +184,8 @@ class Song(Base):
|
||||
)
|
||||
|
||||
band: Mapped[Band] = relationship("Band", back_populates="songs")
|
||||
session: Mapped[Optional[RehearsalSession]] = relationship("RehearsalSession", back_populates="songs")
|
||||
creator: Mapped[Optional[Member]] = relationship("Member", back_populates="authored_songs")
|
||||
session: Mapped[RehearsalSession | None] = relationship("RehearsalSession", back_populates="songs")
|
||||
creator: Mapped[Member | None] = relationship("Member", back_populates="authored_songs")
|
||||
versions: Mapped[list[AudioVersion]] = relationship(
|
||||
"AudioVersion", back_populates="song", cascade="all, delete-orphan"
|
||||
)
|
||||
@@ -206,8 +205,8 @@ class SongComment(Base):
|
||||
UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
body: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
timestamp: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True)
|
||||
tag: Mapped[Optional[str]] = mapped_column(String(32), nullable=True)
|
||||
timestamp: Mapped[float | None] = mapped_column(Numeric(10, 2), nullable=True)
|
||||
tag: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
@@ -227,16 +226,16 @@ class AudioVersion(Base):
|
||||
UUID(as_uuid=True), ForeignKey("songs.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
label: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
label: Mapped[str | None] = mapped_column(String(255))
|
||||
nc_file_path: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
nc_file_etag: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
cdn_hls_base: Mapped[Optional[str]] = mapped_column(Text)
|
||||
waveform_url: Mapped[Optional[str]] = mapped_column(Text)
|
||||
duration_ms: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
format: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
file_size_bytes: Mapped[Optional[int]] = mapped_column(BigInteger)
|
||||
nc_file_etag: Mapped[str | None] = mapped_column(String(255))
|
||||
cdn_hls_base: Mapped[str | None] = mapped_column(Text)
|
||||
waveform_url: Mapped[str | None] = mapped_column(Text)
|
||||
duration_ms: Mapped[int | None] = mapped_column(Integer)
|
||||
format: Mapped[str | None] = mapped_column(String(10))
|
||||
file_size_bytes: Mapped[int | None] = mapped_column(BigInteger)
|
||||
analysis_status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
|
||||
uploaded_by: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
uploaded_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL")
|
||||
)
|
||||
uploaded_at: Mapped[datetime] = mapped_column(
|
||||
@@ -244,7 +243,7 @@ class AudioVersion(Base):
|
||||
)
|
||||
|
||||
song: Mapped[Song] = relationship("Song", back_populates="versions")
|
||||
uploader: Mapped[Optional[Member]] = relationship(
|
||||
uploader: Mapped[Member | None] = relationship(
|
||||
"Member", back_populates="uploaded_versions"
|
||||
)
|
||||
annotations: Mapped[list[Annotation]] = relationship(
|
||||
@@ -273,16 +272,16 @@ class Annotation(Base):
|
||||
)
|
||||
type: Mapped[str] = mapped_column(String(10), nullable=False) # 'point' | 'range'
|
||||
timestamp_ms: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
range_end_ms: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
body: Mapped[Optional[str]] = mapped_column(Text)
|
||||
voice_note_url: Mapped[Optional[str]] = mapped_column(Text)
|
||||
label: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
range_end_ms: Mapped[int | None] = mapped_column(Integer)
|
||||
body: Mapped[str | None] = mapped_column(Text)
|
||||
voice_note_url: Mapped[str | None] = mapped_column(Text)
|
||||
label: Mapped[str | None] = mapped_column(String(255))
|
||||
tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False)
|
||||
parent_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
parent_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("annotations.id", ondelete="SET NULL")
|
||||
)
|
||||
resolved: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
@@ -297,13 +296,13 @@ class Annotation(Base):
|
||||
replies: Mapped[list[Annotation]] = relationship(
|
||||
"Annotation", foreign_keys=[parent_id], back_populates="parent"
|
||||
)
|
||||
parent: Mapped[Optional[Annotation]] = relationship(
|
||||
parent: Mapped[Annotation | None] = relationship(
|
||||
"Annotation", foreign_keys=[parent_id], back_populates="replies", remote_side=[id]
|
||||
)
|
||||
reactions: Mapped[list[Reaction]] = relationship(
|
||||
"Reaction", back_populates="annotation", cascade="all, delete-orphan"
|
||||
)
|
||||
range_analysis: Mapped[Optional[RangeAnalysis]] = relationship(
|
||||
range_analysis: Mapped[RangeAnalysis | None] = relationship(
|
||||
"RangeAnalysis", back_populates="annotation", uselist=False
|
||||
)
|
||||
|
||||
@@ -329,19 +328,19 @@ class RangeAnalysis(Base):
|
||||
)
|
||||
start_ms: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
end_ms: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
bpm: Mapped[Optional[float]] = mapped_column(Numeric(7, 2))
|
||||
bpm_confidence: Mapped[Optional[float]] = mapped_column(Numeric(4, 3))
|
||||
key: Mapped[Optional[str]] = mapped_column(String(30))
|
||||
key_confidence: Mapped[Optional[float]] = mapped_column(Numeric(4, 3))
|
||||
scale: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
avg_loudness_lufs: Mapped[Optional[float]] = mapped_column(Numeric(6, 2))
|
||||
peak_loudness_dbfs: Mapped[Optional[float]] = mapped_column(Numeric(6, 2))
|
||||
spectral_centroid: Mapped[Optional[float]] = mapped_column(Numeric(10, 2))
|
||||
energy: Mapped[Optional[float]] = mapped_column(Numeric(5, 4))
|
||||
danceability: Mapped[Optional[float]] = mapped_column(Numeric(5, 4))
|
||||
chroma_vector: Mapped[Optional[list[float]]] = mapped_column(ARRAY(Numeric))
|
||||
mfcc_mean: Mapped[Optional[list[float]]] = mapped_column(ARRAY(Numeric))
|
||||
analysis_version: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
bpm: Mapped[float | None] = mapped_column(Numeric(7, 2))
|
||||
bpm_confidence: Mapped[float | None] = mapped_column(Numeric(4, 3))
|
||||
key: Mapped[str | None] = mapped_column(String(30))
|
||||
key_confidence: Mapped[float | None] = mapped_column(Numeric(4, 3))
|
||||
scale: Mapped[str | None] = mapped_column(String(10))
|
||||
avg_loudness_lufs: Mapped[float | None] = mapped_column(Numeric(6, 2))
|
||||
peak_loudness_dbfs: Mapped[float | None] = mapped_column(Numeric(6, 2))
|
||||
spectral_centroid: Mapped[float | None] = mapped_column(Numeric(10, 2))
|
||||
energy: Mapped[float | None] = mapped_column(Numeric(5, 4))
|
||||
danceability: Mapped[float | None] = mapped_column(Numeric(5, 4))
|
||||
chroma_vector: Mapped[list[float] | None] = mapped_column(ARRAY(Numeric))
|
||||
mfcc_mean: Mapped[list[float] | None] = mapped_column(ARRAY(Numeric))
|
||||
analysis_version: Mapped[str | None] = mapped_column(String(20))
|
||||
computed_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
@@ -393,9 +392,9 @@ class Job(Base):
|
||||
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="queued", index=True)
|
||||
attempt: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
error: Mapped[Optional[str]] = mapped_column(Text)
|
||||
error: Mapped[str | None] = mapped_column(Text)
|
||||
queued_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
started_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
@@ -10,8 +10,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from rehearsalhub.db.engine import get_session
|
||||
from rehearsalhub.db.models import Member
|
||||
from rehearsalhub.services.auth import decode_token
|
||||
from rehearsalhub.repositories.member import MemberRepository
|
||||
from rehearsalhub.services.auth import decode_token
|
||||
|
||||
# auto_error=False so we can fall back to cookie auth without a 401 from the scheme itself
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", auto_error=False)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""RehearsalHub FastAPI application entry point."""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
@@ -15,8 +15,8 @@ from rehearsalhub.routers import (
|
||||
annotations_router,
|
||||
auth_router,
|
||||
bands_router,
|
||||
invites_router,
|
||||
internal_router,
|
||||
invites_router,
|
||||
members_router,
|
||||
sessions_router,
|
||||
songs_router,
|
||||
|
||||
@@ -11,7 +11,7 @@ never reads a job ID that isn't yet visible in the DB.
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
import redis.asyncio as aioredis
|
||||
@@ -60,7 +60,7 @@ class RedisJobQueue:
|
||||
job = await self._session.get(Job, job_id)
|
||||
if job:
|
||||
job.status = "running"
|
||||
job.started_at = datetime.now(timezone.utc)
|
||||
job.started_at = datetime.now(UTC)
|
||||
job.attempt = (job.attempt or 0) + 1
|
||||
await self._session.flush()
|
||||
|
||||
@@ -68,7 +68,7 @@ class RedisJobQueue:
|
||||
job = await self._session.get(Job, job_id)
|
||||
if job:
|
||||
job.status = "done"
|
||||
job.finished_at = datetime.now(timezone.utc)
|
||||
job.finished_at = datetime.now(UTC)
|
||||
await self._session.flush()
|
||||
|
||||
async def mark_failed(self, job_id: uuid.UUID, error: str) -> None:
|
||||
@@ -76,7 +76,7 @@ class RedisJobQueue:
|
||||
if job:
|
||||
job.status = "failed"
|
||||
job.error = error[:2000]
|
||||
job.finished_at = datetime.now(timezone.utc)
|
||||
job.finished_at = datetime.now(UTC)
|
||||
await self._session.flush()
|
||||
|
||||
async def dequeue(self, timeout: int = 5) -> tuple[uuid.UUID, str, dict[str, Any]] | None:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import UTC
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import and_, select
|
||||
@@ -31,9 +32,9 @@ class AnnotationRepository(BaseRepository[Annotation]):
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def soft_delete(self, annotation: Annotation) -> None:
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
|
||||
annotation.deleted_at = datetime.now(timezone.utc)
|
||||
annotation.deleted_at = datetime.now(UTC)
|
||||
await self.session.flush()
|
||||
|
||||
async def search_ranges(
|
||||
@@ -45,7 +46,7 @@ class AnnotationRepository(BaseRepository[Annotation]):
|
||||
tag: str | None = None,
|
||||
min_duration_ms: int | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
from rehearsalhub.db.models import AudioVersion, RangeAnalysis, Song
|
||||
from rehearsalhub.db.models import AudioVersion, Song
|
||||
|
||||
conditions = [
|
||||
Song.band_id == band_id,
|
||||
|
||||
@@ -37,7 +37,7 @@ class AudioVersionRepository(BaseRepository[AudioVersion]):
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_with_annotations(self, version_id: uuid.UUID) -> AudioVersion | None:
|
||||
from rehearsalhub.db.models import Annotation, RangeAnalysis
|
||||
from rehearsalhub.db.models import Annotation
|
||||
|
||||
stmt = (
|
||||
select(AudioVersion)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from rehearsalhub.db.models import Band, BandInvite, BandMember
|
||||
from rehearsalhub.repositories.base import BaseRepository
|
||||
|
||||
@@ -69,7 +68,7 @@ class BandRepository(BaseRepository[Band]):
|
||||
token=secrets.token_urlsafe(32),
|
||||
role=role,
|
||||
created_by=created_by,
|
||||
expires_at=datetime.now(timezone.utc) + timedelta(hours=ttl_hours),
|
||||
expires_at=datetime.now(UTC) + timedelta(hours=ttl_hours),
|
||||
)
|
||||
self.session.add(invite)
|
||||
await self.session.flush()
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any, Generic, Sequence, TypeVar
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
@@ -24,7 +24,7 @@ class JobRepository(BaseRepository[Job]):
|
||||
job = await self.get_by_id(job_id)
|
||||
if job:
|
||||
job.status = "running"
|
||||
job.started_at = datetime.now(timezone.utc)
|
||||
job.started_at = datetime.now(UTC)
|
||||
job.attempt = (job.attempt or 0) + 1
|
||||
await self.session.flush()
|
||||
return job
|
||||
@@ -33,7 +33,7 @@ class JobRepository(BaseRepository[Job]):
|
||||
job = await self.get_by_id(job_id)
|
||||
if job:
|
||||
job.status = "done"
|
||||
job.finished_at = datetime.now(timezone.utc)
|
||||
job.finished_at = datetime.now(UTC)
|
||||
await self.session.flush()
|
||||
return job
|
||||
|
||||
@@ -42,6 +42,6 @@ class JobRepository(BaseRepository[Job]):
|
||||
if job:
|
||||
job.status = "failed"
|
||||
job.error = error[:2000]
|
||||
job.finished_at = datetime.now(timezone.utc)
|
||||
job.finished_at = datetime.now(UTC)
|
||||
await self.session.flush()
|
||||
return job
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -32,12 +31,12 @@ class SongRepository(BaseRepository[Song]):
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_by_nc_folder_path(self, nc_folder_path: str) -> "Song | None":
|
||||
async def get_by_nc_folder_path(self, nc_folder_path: str) -> Song | None:
|
||||
stmt = select(Song).where(Song.nc_folder_path == nc_folder_path)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_by_title_and_band(self, band_id: uuid.UUID, title: str) -> "Song | None":
|
||||
async def get_by_title_and_band(self, band_id: uuid.UUID, title: str) -> Song | None:
|
||||
stmt = select(Song).where(Song.band_id == band_id, Song.title == title)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
@@ -53,9 +52,8 @@ class SongRepository(BaseRepository[Song]):
|
||||
session_id: uuid.UUID | None = None,
|
||||
unattributed: bool = False,
|
||||
) -> list[Song]:
|
||||
from sqlalchemy import cast, func
|
||||
from sqlalchemy import Text, cast, func
|
||||
from sqlalchemy.dialects.postgresql import ARRAY
|
||||
from sqlalchemy import Text
|
||||
|
||||
stmt = (
|
||||
select(Song)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from rehearsalhub.routers.annotations import router as annotations_router
|
||||
from rehearsalhub.routers.auth import router as auth_router
|
||||
from rehearsalhub.routers.bands import router as bands_router
|
||||
from rehearsalhub.routers.invites import router as invites_router
|
||||
from rehearsalhub.routers.internal import router as internal_router
|
||||
from rehearsalhub.routers.invites import router as invites_router
|
||||
from rehearsalhub.routers.members import router as members_router
|
||||
from rehearsalhub.routers.sessions import router as sessions_router
|
||||
from rehearsalhub.routers.songs import router as songs_router
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from rehearsalhub.db.engine import get_session
|
||||
from rehearsalhub.db.models import BandInvite, Member
|
||||
from rehearsalhub.db.models import Member
|
||||
from rehearsalhub.dependencies import get_current_member
|
||||
from rehearsalhub.schemas.band import BandCreate, BandRead, BandReadWithMembers, BandUpdate
|
||||
from rehearsalhub.schemas.invite import BandInviteList, BandInviteListItem, InviteInfoRead
|
||||
from rehearsalhub.repositories.band import BandRepository
|
||||
from rehearsalhub.schemas.band import BandCreate, BandRead, BandReadWithMembers, BandUpdate
|
||||
from rehearsalhub.schemas.invite import BandInviteList, BandInviteListItem
|
||||
from rehearsalhub.services.band import BandService
|
||||
from rehearsalhub.storage.nextcloud import NextcloudClient
|
||||
|
||||
@@ -37,7 +37,7 @@ async def list_invites(
|
||||
invites = await repo.get_invites_for_band(band_id)
|
||||
|
||||
# Filter for non-expired invites (optional - could also show expired)
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
pending_invites = [
|
||||
invite for invite in invites
|
||||
if invite.expires_at > now and invite.used_at is None
|
||||
@@ -93,7 +93,7 @@ async def revoke_invite(
|
||||
)
|
||||
|
||||
# Check if invite is still pending (not used and not expired)
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
if invite.used_at is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
"""
|
||||
Invite management endpoints.
|
||||
"""
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from rehearsalhub.db.engine import get_session
|
||||
from rehearsalhub.db.models import BandInvite, Member
|
||||
from rehearsalhub.schemas.invite import InviteInfoRead
|
||||
from rehearsalhub.repositories.band import BandRepository
|
||||
from rehearsalhub.schemas.invite import InviteInfoRead
|
||||
|
||||
router = APIRouter(prefix="/invites", tags=["invites"])
|
||||
|
||||
@@ -32,7 +30,7 @@ async def get_invite_info(
|
||||
)
|
||||
|
||||
# Check if invite is already used or expired
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
if invite.used_at is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -96,7 +96,7 @@ async def accept_invite(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invite not found")
|
||||
if invite.used_at is not None:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Invite already used")
|
||||
if invite.expires_at < datetime.now(timezone.utc):
|
||||
if invite.expires_at < datetime.now(UTC):
|
||||
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Invite expired")
|
||||
|
||||
# Idempotent — already a member
|
||||
@@ -107,7 +107,7 @@ async def accept_invite(
|
||||
bm = await repo.add_member(invite.band_id, current_member.id, role=invite.role)
|
||||
|
||||
# Mark invite as used
|
||||
invite.used_at = datetime.now(timezone.utc)
|
||||
invite.used_at = datetime.now(UTC)
|
||||
invite.used_by = current_member.id
|
||||
await session.flush()
|
||||
|
||||
@@ -123,8 +123,9 @@ async def accept_invite(
|
||||
@router.get("/invites/{token}", response_model=BandInviteRead)
|
||||
async def get_invite(token: str, session: AsyncSession = Depends(get_session)):
|
||||
"""Preview invite info (band name etc.) before accepting — no auth required."""
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from rehearsalhub.db.models import BandInvite
|
||||
stmt = select(BandInvite).options(selectinload(BandInvite.band)).where(BandInvite.token == token)
|
||||
result = await session.execute(stmt)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
@@ -11,10 +10,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from rehearsalhub.db.engine import get_session, get_session_factory
|
||||
from rehearsalhub.db.models import Member
|
||||
from rehearsalhub.dependencies import get_current_member
|
||||
from rehearsalhub.routers.versions import _member_from_request
|
||||
from rehearsalhub.repositories.band import BandRepository
|
||||
from rehearsalhub.repositories.comment import CommentRepository
|
||||
from rehearsalhub.repositories.song import SongRepository
|
||||
from rehearsalhub.routers.versions import _member_from_request
|
||||
from rehearsalhub.schemas.comment import SongCommentCreate, SongCommentRead
|
||||
from rehearsalhub.schemas.song import SongCreate, SongRead, SongUpdate
|
||||
from rehearsalhub.services.band import BandService
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import uuid
|
||||
import asyncio
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import uuid
|
||||
|
||||
from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect
|
||||
|
||||
from rehearsalhub.repositories.member import MemberRepository
|
||||
from rehearsalhub.db.engine import get_session
|
||||
from rehearsalhub.repositories.member import MemberRepository
|
||||
from rehearsalhub.services.auth import decode_token
|
||||
from rehearsalhub.ws import manager
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from rehearsalhub.schemas.annotation import (
|
||||
)
|
||||
from rehearsalhub.schemas.audio_version import AudioVersionCreate, AudioVersionRead
|
||||
from rehearsalhub.schemas.auth import LoginRequest, RegisterRequest, TokenResponse
|
||||
from rehearsalhub.schemas.band import BandCreate, BandRead, BandReadWithMembers, BandMemberRead
|
||||
from rehearsalhub.schemas.band import BandCreate, BandMemberRead, BandRead, BandReadWithMembers
|
||||
from rehearsalhub.schemas.member import MemberRead
|
||||
from rehearsalhub.schemas.song import SongCreate, SongRead, SongUpdate
|
||||
|
||||
|
||||
@@ -26,15 +26,15 @@ class SongCommentRead(BaseModel):
|
||||
created_at: datetime
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, c: object) -> "SongCommentRead":
|
||||
def from_model(cls, c: object) -> SongCommentRead:
|
||||
return cls(
|
||||
id=getattr(c, "id"),
|
||||
song_id=getattr(c, "song_id"),
|
||||
body=getattr(c, "body"),
|
||||
author_id=getattr(c, "author_id"),
|
||||
author_name=getattr(getattr(c, "author"), "display_name"),
|
||||
author_avatar_url=getattr(getattr(c, "author"), "avatar_url"),
|
||||
timestamp=getattr(c, "timestamp"),
|
||||
id=c.id,
|
||||
song_id=c.song_id,
|
||||
body=c.body,
|
||||
author_id=c.author_id,
|
||||
author_name=c.author.display_name,
|
||||
author_avatar_url=c.author.avatar_url,
|
||||
timestamp=c.timestamp,
|
||||
tag=getattr(c, "tag", None),
|
||||
created_at=getattr(c, "created_at"),
|
||||
created_at=c.created_at,
|
||||
)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr
|
||||
|
||||
|
||||
class MemberBase(BaseModel):
|
||||
@@ -23,7 +22,7 @@ class MemberRead(MemberBase):
|
||||
def from_model(cls, m: object) -> "MemberRead":
|
||||
obj = cls.model_validate(m)
|
||||
obj.nc_configured = bool(
|
||||
getattr(m, "nc_url") and getattr(m, "nc_username") and getattr(m, "nc_password")
|
||||
m.nc_url and m.nc_username and m.nc_password
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import bcrypt
|
||||
from jose import JWTError, jwt
|
||||
@@ -25,12 +25,12 @@ def verify_password(plain: str, hashed: str) -> bool:
|
||||
|
||||
def create_access_token(member_id: str, email: str) -> str:
|
||||
settings = get_settings()
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes)
|
||||
expire = datetime.now(UTC) + timedelta(minutes=settings.access_token_expire_minutes)
|
||||
payload = {
|
||||
"sub": member_id,
|
||||
"email": email,
|
||||
"exp": expire,
|
||||
"iat": datetime.now(timezone.utc),
|
||||
"iat": datetime.now(UTC),
|
||||
}
|
||||
return jwt.encode(payload, settings.secret_key, algorithm=settings.jwt_algorithm)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Avatar generation service using DiceBear API."""
|
||||
|
||||
from typing import Optional
|
||||
import httpx
|
||||
|
||||
|
||||
from rehearsalhub.db.models import Member
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class AvatarService:
|
||||
"""
|
||||
return await self.generate_avatar_url(str(member.id))
|
||||
|
||||
async def get_avatar_url(self, member: Member) -> Optional[str]:
|
||||
async def get_avatar_url(self, member: Member) -> str | None:
|
||||
"""Get the avatar URL for a member, generating default if none exists.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from rehearsalhub.db.models import Band
|
||||
from rehearsalhub.repositories.band import BandRepository
|
||||
from rehearsalhub.schemas.band import BandCreate, BandReadWithMembers
|
||||
from rehearsalhub.schemas.band import BandCreate
|
||||
from rehearsalhub.storage.nextcloud import NextcloudClient
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator
|
||||
from urllib.parse import unquote
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from rehearsalhub.db.models import Member
|
||||
from rehearsalhub.repositories.audio_version import AudioVersionRepository
|
||||
from rehearsalhub.repositories.rehearsal_session import RehearsalSessionRepository
|
||||
from rehearsalhub.repositories.song import SongRepository
|
||||
|
||||
@@ -9,7 +9,7 @@ from rehearsalhub.queue.redis_queue import RedisJobQueue
|
||||
from rehearsalhub.repositories.audio_version import AudioVersionRepository
|
||||
from rehearsalhub.repositories.song import SongRepository
|
||||
from rehearsalhub.schemas.audio_version import AudioVersionCreate
|
||||
from rehearsalhub.schemas.song import SongCreate, SongRead, SongUpdate
|
||||
from rehearsalhub.schemas.song import SongCreate, SongRead
|
||||
from rehearsalhub.storage.nextcloud import NextcloudClient
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from rehearsalhub.config import get_settings
|
||||
from rehearsalhub.storage.protocol import FileMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,7 +29,7 @@ class NextcloudClient:
|
||||
self._dav_root = f"{self._base}/remote.php/dav/files/{self._auth[0]}"
|
||||
|
||||
@classmethod
|
||||
def for_member(cls, member: object) -> "NextcloudClient | None":
|
||||
def for_member(cls, member: object) -> NextcloudClient | None:
|
||||
"""Return a client using member's personal NC credentials if configured.
|
||||
Returns None if member has no Nextcloud configuration."""
|
||||
nc_url = getattr(member, "nc_url", None)
|
||||
|
||||
Reference in New Issue
Block a user