refactor: restructure project into layered architecture

This commit is contained in:
2026-05-17 03:36:28 +03:30
parent b4971fcb6a
commit be6e1fab8e
29 changed files with 240 additions and 302 deletions

View File

@@ -1,40 +0,0 @@
import sys
from os.path import abspath, dirname
import logging.config
from alembic import context
sys.path.insert(0, dirname(dirname(abspath(__file__))))
from config import DATABASE_URL, LOGGING_CONFIG
from database import Base, engine
import models
#log-settings
logging.config.dictConfig(LOGGING_CONFIG)
target_metadata = Base.metadata
def run_migrations_offline():
context.configure(
url=DATABASE_URL,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
with engine.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

1
backend/.env Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL=sqlite:///./wheel.db

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -5,7 +5,7 @@
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = alembic
script_location = %(here)s/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time

View File

@@ -5,76 +5,18 @@ from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
from app.db.session import Base
from config import settings
from app.db import models
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
config.set_main_option(
"sqlalchemy.url",
settings.DATABASE_URL
)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from database import Base, engine
import models
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
target_metadata = Base.metadata

View File

@@ -0,0 +1,28 @@
"""initial
Revision ID: 96dff3d4cf1c
Revises:
Create Date: 2026-05-17 03:07:55.231096
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '96dff3d4cf1c'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass

Binary file not shown.

Binary file not shown.

View File

@@ -1,37 +0,0 @@
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from fastapi.middleware.cors import CORSMiddleware
import models
import schemas
import crud
from database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# DELETE ITEM
@app.delete("/items/{item_id}")
def delete_item(
item_id: int,
db: Session = Depends(get_db)
):
item = crud.delete_item(db, item_id)
if not item:
raise HTTPException(
status_code=404,
detail="Item not found"
)
return {"message": "Item deleted"}
# CREATE ITEM
@app.post("/items", response_model=schemas.ItemResponse)
def create_item(
item: schemas.ItemCreate,
db: Session = Depends(get_db)
):
return crud.create_item(db, item.title)

64
backend/api/admin.py Normal file
View File

@@ -0,0 +1,64 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
import schemas
from services import crud
from app.db.session import SessionLocal
router = APIRouter()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# CREATE ITEM
@router.post(
"/items",
response_model=schemas.ItemResponse
)
def create_item(
item: schemas.ItemCreate,
db: Session = Depends(get_db)
):
return crud.create_item(
db,
item.title
)
# DELETE ITEM
@router.delete("/items/{item_id}")
def delete_item(
item_id: int,
db: Session = Depends(get_db)
):
item = crud.delete_item(
db,
item_id
)
if not item:
raise HTTPException(
status_code=404,
detail="Item not found"
)
return {
"message": "Item deleted"
}

View File

@@ -1,38 +0,0 @@
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from fastapi.middleware.cors import CORSMiddleware
import models
import schemas
import crud
from database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# SPIN
@app.post("/spin")
def spin(db: Session = Depends(get_db)):
winner = crud.spin_wheel(db)
if not winner:
raise HTTPException(
status_code=400,
detail="No items available"
)
return {
"winner": {
"id": winner.id,
"title": winner.title
}
}
# GET ITEMS
@app.get("/items", response_model=list[schemas.ItemResponse])
def get_items(db: Session = Depends(get_db)):
return crud.get_items(db)

59
backend/api/user.py Normal file
View File

@@ -0,0 +1,59 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
import schemas
from services import crud
from app.db.session import SessionLocal
router = APIRouter()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# GET ITEMS
@router.get(
"/items",
response_model=list[schemas.ItemResponse]
)
def get_items(
db: Session = Depends(get_db)
):
return crud.get_items(db)
# SPIN
@router.post("/spin")
def spin(
db: Session = Depends(get_db)
):
winner = crud.spin_wheel(db)
if not winner:
raise HTTPException(
status_code=400,
detail="No items available"
)
return {
"winner": {
"id": winner.id,
"title": winner.title
}
}

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,87 +0,0 @@
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from fastapi.middleware.cors import CORSMiddleware
import models
import schemas
import crud
from database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Database Session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# GET ITEMS
@app.get("/items", response_model=list[schemas.ItemResponse])
def get_items(db: Session = Depends(get_db)):
return crud.get_items(db)
# CREATE ITEM
@app.post("/items", response_model=schemas.ItemResponse)
def create_item(
item: schemas.ItemCreate,
db: Session = Depends(get_db)
):
return crud.create_item(db, item.title)
# DELETE ITEM
@app.delete("/items/{item_id}")
def delete_item(
item_id: int,
db: Session = Depends(get_db)
):
item = crud.delete_item(db, item_id)
if not item:
raise HTTPException(
status_code=404,
detail="Item not found"
)
return {"message": "Item deleted"}
# SPIN
@app.post("/spin")
def spin(db: Session = Depends(get_db)):
winner = crud.spin_wheel(db)
if not winner:
raise HTTPException(
status_code=400,
detail="No items available"
)
return {
"winner": {
"id": winner.id,
"title": winner.title
}
}
from sqlalchemy import Column, Integer, String
from database import Base
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)

21
backend/app/db/models.py Normal file
View File

@@ -0,0 +1,21 @@
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import String
from app.db.session import Base
class Item(Base):
__tablename__ = "items"
id = Column(
Integer,
primary_key=True,
index=True
)
title = Column(
String,
nullable=False
)

View File

@@ -1,11 +1,11 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "sqlite:///./wheel.db"
from config import settings
engine = create_engine(
DATABASE_URL,
settings.DATABASE_URL,
connect_args={"check_same_thread": False}
)
@@ -15,13 +15,4 @@ SessionLocal = sessionmaker(
bind=engine
)
# Database Session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Base = declarative_base()

10
backend/config.py Normal file
View File

@@ -0,0 +1,10 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -1,18 +1,13 @@
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import models
import schemas
import crud
from api.admin import router as admin_router
from api.user import router as user_router
from database import SessionLocal, engine
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
@@ -22,6 +17,6 @@ app.add_middleware(
)
app.include_router(admin_router)
app.include_router(user_router)
app.include_router(admin_router)
app.include_router(user_router)

BIN
backend/requirements.txt Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,32 +1,61 @@
from sqlalchemy.orm import Session
from models import Item
import random
def get_items(db: Session):
from sqlalchemy.orm import Session
from app.db.models import Item
def get_items(
db: Session
):
return db.query(Item).all()
def create_item(db: Session, title: str):
item = Item(title=title)
def create_item(
db: Session,
title: str
):
item = Item(
title=title
)
db.add(item)
db.commit()
db.refresh(item)
return item
def delete_item(db: Session, item_id: int):
item = db.query(Item).filter(Item.id == item_id).first()
def delete_item(
db: Session,
item_id: int
):
item = db.query(Item).filter(
Item.id == item_id
).first()
if item:
db.delete(item)
db.commit()
return item
def spin_wheel(db: Session):
def spin_wheel(
db: Session
):
items = db.query(Item).all()
if not items:
return None
return random.choice(items)