Refactor code structure for improved readability and maintainability
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ wheels/
|
|||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
.env
|
||||||
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""App package."""
|
||||||
1
app/core/__init__.py
Normal file
1
app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Core package."""
|
||||||
23
app/core/config.py
Normal file
23
app/core/config.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""Application settings."""
|
||||||
|
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
env_ignore_empty=True,
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
|
||||||
|
HOST: str = "localhost"
|
||||||
|
PORT: int = 8000
|
||||||
|
RELOAD: bool = True
|
||||||
|
LOG_LEVEL: str = "info"
|
||||||
|
|
||||||
|
DATABASE_URL: str = "sqlite+aiosqlite:///data/soundboard.db"
|
||||||
|
DATABASE_ECHO: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
38
app/core/database.py
Normal file
38
app/core/database.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.models import ( # noqa: F401
|
||||||
|
plan,
|
||||||
|
playlist,
|
||||||
|
playlist_sound,
|
||||||
|
sound,
|
||||||
|
sound_played,
|
||||||
|
stream,
|
||||||
|
user,
|
||||||
|
user_oauth,
|
||||||
|
)
|
||||||
|
|
||||||
|
engine: AsyncEngine = create_async_engine(
|
||||||
|
settings.DATABASE_URL,
|
||||||
|
echo=settings.DATABASE_ECHO,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
async with AsyncSession(engine) as session:
|
||||||
|
try:
|
||||||
|
yield session
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def init_db() -> None:
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(SQLModel.metadata.create_all)
|
||||||
26
app/main.py
Normal file
26
app/main.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from app.core.database import init_db
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
|
||||||
|
|
||||||
|
await init_db()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def health() -> dict[str, str]:
|
||||||
|
return {"status": "healthy"}
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
5
app/middleware/__init__.py
Normal file
5
app/middleware/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Middleware package."""
|
||||||
|
|
||||||
|
from app.middleware.logging import LoggingMiddleware
|
||||||
|
|
||||||
|
__all__ = ["LoggingMiddleware"]
|
||||||
1
app/models/__init__.py
Normal file
1
app/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Models package."""
|
||||||
13
app/models/base.py
Normal file
13
app/models/base.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(SQLModel):
|
||||||
|
"""Base model with common fields for all models."""
|
||||||
|
|
||||||
|
id: int | None = Field(primary_key=True, default=None)
|
||||||
|
|
||||||
|
# timestamps
|
||||||
|
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||||
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||||
21
app/models/plan.py
Normal file
21
app/models/plan.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Plan(BaseModel, table=True):
|
||||||
|
"""Database model for a plan."""
|
||||||
|
|
||||||
|
code: str = Field(index=True, unique=True, nullable=False)
|
||||||
|
name: str = Field(nullable=False)
|
||||||
|
description: str | None = Field(default=None)
|
||||||
|
credits: int = Field(default=0, ge=0, nullable=False)
|
||||||
|
max_credits: int = Field(default=0, ge=0, nullable=False)
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
users: list["User"] = Relationship(back_populates="plan")
|
||||||
26
app/models/playlist.py
Normal file
26
app/models/playlist.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.playlist_sound import PlaylistSound
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Playlist(BaseModel, table=True):
|
||||||
|
"""Database model for a playlist."""
|
||||||
|
|
||||||
|
user_id: int | None = Field(foreign_key="user.id", default=None)
|
||||||
|
name: str = Field(unique=True, nullable=False)
|
||||||
|
description: str | None = Field(default=None)
|
||||||
|
genre: str | None = Field(default=None)
|
||||||
|
is_main: bool = Field(default=False, nullable=False)
|
||||||
|
is_current: bool = Field(default=False, nullable=False)
|
||||||
|
is_deletable: bool = Field(default=True, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
user: "User" = Relationship(back_populates="playlists")
|
||||||
|
playlist_sounds: list["PlaylistSound"] = Relationship(back_populates="playlist")
|
||||||
37
app/models/playlist_sound.py
Normal file
37
app/models/playlist_sound.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship, UniqueConstraint
|
||||||
|
|
||||||
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.playlist import Playlist
|
||||||
|
from app.models.sound import Sound
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistSound(BaseModel, table=True):
|
||||||
|
"""Database model for a sound in a playlist."""
|
||||||
|
|
||||||
|
__tablename__ = "playlist_sound" # pyright: ignore[reportAssignmentType]
|
||||||
|
|
||||||
|
playlist_id: int = Field(foreign_key="playlist.id", nullable=False)
|
||||||
|
sound_id: int = Field(foreign_key="sound.id", nullable=False)
|
||||||
|
position: int = Field(default=0, ge=0, nullable=False)
|
||||||
|
|
||||||
|
# constraints
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"playlist_id",
|
||||||
|
"sound_id",
|
||||||
|
name="uq_playlist_sound_playlist_sound",
|
||||||
|
),
|
||||||
|
UniqueConstraint(
|
||||||
|
"playlist_id",
|
||||||
|
"position",
|
||||||
|
name="uq_playlist_sound_playlist_position",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
playlist: "Playlist" = Relationship(back_populates="playlist_sounds")
|
||||||
|
sound: "Sound" = Relationship(back_populates="playlist_sounds")
|
||||||
34
app/models/sound.py
Normal file
34
app/models/sound.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.playlist_sound import PlaylistSound
|
||||||
|
from app.models.stream import Stream
|
||||||
|
|
||||||
|
|
||||||
|
class Sound(BaseModel, table=True):
|
||||||
|
"""Database model for a sound."""
|
||||||
|
|
||||||
|
type: str = Field(nullable=False)
|
||||||
|
name: str = Field(nullable=False)
|
||||||
|
filename: str = Field(nullable=False)
|
||||||
|
duration: int = Field(default=0, ge=0, nullable=False)
|
||||||
|
size: int = Field(default=0, ge=0, nullable=False)
|
||||||
|
hash: str = Field(nullable=False)
|
||||||
|
normalized_filename: str | None = Field(default=None)
|
||||||
|
normalized_duration: int | None = Field(default=None, ge=0)
|
||||||
|
normalized_size: int | None = Field(default=None, ge=0)
|
||||||
|
normalized_hash: str | None = Field(default=None)
|
||||||
|
thumbnail: str | None = Field(default=None)
|
||||||
|
play_count: int = Field(default=0, ge=0, nullable=False)
|
||||||
|
is_normalized: bool = Field(default=False, nullable=False)
|
||||||
|
is_music: bool = Field(default=False, nullable=False)
|
||||||
|
is_deletable: bool = Field(default=True, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
playlist_sounds: list["PlaylistSound"] = Relationship(back_populates="sound")
|
||||||
|
streams: list["Stream"] = Relationship(back_populates="sound")
|
||||||
32
app/models/sound_played.py
Normal file
32
app/models/sound_played.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship, UniqueConstraint
|
||||||
|
|
||||||
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.sound import Sound
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class SoundPlayed(BaseModel, table=True):
|
||||||
|
"""Database model for a sound played."""
|
||||||
|
|
||||||
|
__tablename__ = "sound_played" # pyright: ignore[reportAssignmentType]
|
||||||
|
|
||||||
|
user_id: int = Field(foreign_key="user.id", nullable=False)
|
||||||
|
sound_id: int = Field(foreign_key="sound.id", nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
# constraints
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"user_id",
|
||||||
|
"sound_id",
|
||||||
|
name="uq_sound_played_user_sound",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
user: "User" = Relationship(back_populates="sounds_played")
|
||||||
|
sound: "Sound" = Relationship(back_populates="play_history")
|
||||||
40
app/models/stream.py
Normal file
40
app/models/stream.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship, UniqueConstraint
|
||||||
|
|
||||||
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.sound import Sound
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Stream(BaseModel, table=True):
|
||||||
|
"""Database model for a stream."""
|
||||||
|
|
||||||
|
service: str = Field(nullable=False)
|
||||||
|
service_id: str = Field(nullable=False)
|
||||||
|
user_id: int = Field(foreign_key="user.id", nullable=False)
|
||||||
|
sound_id: int | None = Field(foreign_key="sound.id", default=None)
|
||||||
|
url: str = Field(nullable=False)
|
||||||
|
title: str | None = Field(default=None)
|
||||||
|
track: str | None = Field(default=None)
|
||||||
|
artist: str | None = Field(default=None)
|
||||||
|
album: str | None = Field(default=None)
|
||||||
|
genre: str | None = Field(default=None)
|
||||||
|
status: str = Field(nullable=False, default="pending")
|
||||||
|
error: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
# constraints
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"service",
|
||||||
|
"service_id",
|
||||||
|
name="uq_stream_service_service_id",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
sound: "Sound" = Relationship(back_populates="streams")
|
||||||
|
user: "User" = Relationship(back_populates="streams")
|
||||||
32
app/models/user.py
Normal file
32
app/models/user.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship
|
||||||
|
|
||||||
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.plan import Plan
|
||||||
|
from app.models.playlist import Playlist
|
||||||
|
from app.models.user_oauth import UserOauth
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel, table=True):
|
||||||
|
"""Database model for a user."""
|
||||||
|
|
||||||
|
plan_id: int = Field(foreign_key="plan.id")
|
||||||
|
role: str = Field(nullable=False, default="user")
|
||||||
|
email: str = Field(unique=True, nullable=False)
|
||||||
|
name: str = Field(nullable=False)
|
||||||
|
picture: str | None = Field(default=None)
|
||||||
|
password_hash: str | None = Field(default=None)
|
||||||
|
is_active: bool = Field(nullable=False, default=True)
|
||||||
|
credits: int = Field(default=0, ge=0, nullable=False)
|
||||||
|
api_token: str | None = Field(unique=True, default=None)
|
||||||
|
api_token_expires_at: datetime | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
oauths: list["UserOauth"] = Relationship(back_populates="user")
|
||||||
|
plan: "Plan" = Relationship(back_populates="users")
|
||||||
|
playlists: list["Playlist"] = Relationship(back_populates="user")
|
||||||
34
app/models/user_oauth.py
Normal file
34
app/models/user_oauth.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship, UniqueConstraint
|
||||||
|
|
||||||
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserOauth(BaseModel, table=True):
|
||||||
|
"""Database model for a user OAuth."""
|
||||||
|
|
||||||
|
__tablename__ = "user_oauth" # pyright: ignore[reportAssignmentType]
|
||||||
|
|
||||||
|
user_id: int = Field(foreign_key="user.id", nullable=False)
|
||||||
|
provider: str = Field(nullable=False)
|
||||||
|
provider_user_id: str = Field(nullable=False)
|
||||||
|
email: str = Field(nullable=False)
|
||||||
|
name: str = Field(nullable=False)
|
||||||
|
picture: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
# constraints
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"provider",
|
||||||
|
"provider_user_id",
|
||||||
|
name="uq_user_oauth_provider_user_id",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
user: "User" = Relationship(back_populates="oauths")
|
||||||
1
app/repositories/__init__.py
Normal file
1
app/repositories/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Repositories package."""
|
||||||
1
app/schemas/__init__.py
Normal file
1
app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Schemas package."""
|
||||||
1
app/services/__init__.py
Normal file
1
app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Services package."""
|
||||||
1
app/utils/__init__.py
Normal file
1
app/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Utils package."""
|
||||||
2
data/.gitignore
vendored
Normal file
2
data/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
6
main.py
6
main.py
@@ -1,6 +0,0 @@
|
|||||||
def main():
|
|
||||||
print("Hello from backend!")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -4,4 +4,31 @@ version = "0.1.0"
|
|||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"aiosqlite==0.21.0",
|
||||||
|
"fastapi[standard]==0.116.1",
|
||||||
|
"pydantic-settings==2.10.1",
|
||||||
|
"uvicorn[standard]==0.35.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
dev-dependencies = [
|
||||||
|
"coverage==7.9.2",
|
||||||
|
"faker==37.4.2",
|
||||||
|
"mypy==1.17.0",
|
||||||
|
"pytest==8.4.1",
|
||||||
|
"ruff==0.12.4",
|
||||||
|
"sqlmodel==0.0.24",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
strict = true
|
||||||
|
exclude = ["venv", ".venv", "alembic"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py312"
|
||||||
|
exclude = ["alembic"]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["ALL"]
|
||||||
|
ignore = ["D100", "D103"]
|
||||||
|
|||||||
12
run.py
Normal file
12
run.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(
|
||||||
|
"app.main:app",
|
||||||
|
host=settings.HOST,
|
||||||
|
port=settings.PORT,
|
||||||
|
reload=settings.RELOAD,
|
||||||
|
log_level=settings.LOG_LEVEL,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user