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
|
||||
.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"
|
||||
readme = "README.md"
|
||||
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