[rewrite] Major changes to code - switched to postgres, added initial backend docker files

This commit is contained in:
Thastertyn committed 2025-03-10 18:02:57 +01:00
1 parent c7e20fc935
commit 71e916586e
52 files changed
+783 -537

No files matched your search

+29 -49
View File
@@ -1,63 +1,43 @@
import logging
from typing import Generator
from contextlib import contextmanager
from typing import Generator
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy import create_engine, text
from sqlalchemy.exc import DatabaseError as SqlAlchemyError
from sqlmodel import Session, create_engine, select, SQLModel
from app.core.config import settings
from app.database.exceptions import DatabaseError
import app.database.models
class DatabaseManager():
logger = logging.getLogger(__name__)
_instance: 'DatabaseManager' = None
def __new__(cls, mysql_user: str, mysql_password: str, mysql_host: str, mysql_port: int, mysql_db_name: str):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
logger.info("Creating engine")
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
SQLModel.metadata.create_all(engine)
def __init__(self, mysql_user: str, mysql_password: str, mysql_host: str, mysql_port: int, mysql_db_name: str) -> None:
self.logger = logging.getLogger(__name__)
self.logger.info("Reading database config")
self.engine = create_engine('mysql+mysqlconnector://%s:%s@%s:%s/%s' % (
mysql_user,
mysql_password,
mysql_host,
str(mysql_port),
mysql_db_name),
pool_pre_ping=True)
self.test_connection()
self.Session = sessionmaker(bind=self.engine) # NOSONAR
def cleanup(self) -> None:
self.logger.debug("Closing connection")
self.engine.dispose()
def test_connection(self):
self.logger.debug("Testing database connection")
try:
with self.engine.connect() as connection:
connection.execute(text("select 1"))
self.logger.debug("Database connection successful")
except DatabaseError as e:
self.logger.critical("Database connection failed: %s", e)
raise DatabaseError("Database connection failed", DatabaseError.CONNECTION_ERROR) from e
def test_connection():
logger.debug("Testing database connection")
try:
with Session(engine) as session:
session.exec(select(1))
logger.debug("Database connection successful")
except SqlAlchemyError as e:
logger.critical("Database connection failed: %s", e)
raise DatabaseError("Database connection failed", DatabaseError.CONNECTION_ERROR) from e
@classmethod
@contextmanager
def get_session(cls) -> Generator[Session, None, None]:
session = cls._instance.Session()
try:
yield session
except Exception as e:
session.rollback()
cls._instance.logger.error("Transaction failed: %s", e)
raise
finally:
session.close()
def cleanup() -> None:
logger.debug("Closing connection")
engine.dispose()
__all__ = ["DatabaseManager"]
@contextmanager
def get_session() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
test_connection()
+6
View File
@@ -0,0 +1,6 @@
from . import user_model, shop_model
__all__ = [
*user_model.__all__,
*shop_model.__all__
]
@@ -0,0 +1,5 @@
from sqlalchemy.orm import declarative_base
Base = declarative_base()
__all__ = ["Base"]
@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, Float, TIMESTAMP, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from .base_model import Base
class Cart(Base):
__tablename__ = "cart"
cart_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
total = Column(Float, nullable=False)
last_updated = Column(TIMESTAMP, onupdate=func.now, nullable=True)
user = relationship("User", back_populates="cart", foreign_keys=[cart_id])
cart_entries = relationship("CartEntry", back_populates="cart")
@@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, TIMESTAMP, Float, ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from .base_model import Base
class Purchase(Base):
__tablename__ = "purchase"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
used_coupon_id = Column(Integer, ForeignKey("coupon.id"), nullable=True)
date_purchased = Column(TIMESTAMP, nullable=True)
total = Column(Float, nullable=True)
user = relationship("User", back_populates="purchases", foreign_keys=[user_id])
coupon = relationship("Coupon", back_populates="purchases", foreign_keys=[used_coupon_id])
purchase_entries = relationship("PurchaseEntry", back_populates="purchase")
Whitespace-only changes.
@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, Float, TIMESTAMP, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from .base_model import Base
class Wishlist(Base):
__tablename__ = "wishlist"
id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
total = Column(Float, nullable=True)
last_updated = Column(TIMESTAMP, onupdate=func.now, nullable=True)
user = relationship("User", back_populates="wishlist", foreign_keys=[id])
wishlist_entries = relationship("WishlistEntry", back_populates="wishlist")
+69
View File
@@ -0,0 +1,69 @@
from enum import Enum as PyEnum
from typing import Optional, List
from datetime import datetime, time
from sqlalchemy import Column, CheckConstraint
from sqlalchemy.dialects.postgresql import JSONB
from sqlmodel import SQLModel, Field, Relationship, Enum
from pydantic import BaseModel, constr
class ShopStatus(PyEnum):
ACTIVE = "active"
INACTIVE = "inactive"
SUSPENDED = "suspended"
class ShopLinkEntry(BaseModel):
name: constr(strip_whitespace=True, min_length=1)
url: constr(strip_whitespace=True, min_length=1)
class ShopLinks(BaseModel):
links: List[ShopLinkEntry]
class ShopBusinessHourEntry(BaseModel):
day: constr(strip_whitespace=True, min_length=1)
open_time: time
close_time: time
class ShopBusinessHours(BaseModel):
hours: List[ShopBusinessHourEntry]
class Shop(SQLModel, table=True):
__tablename__ = 'shop'
id: Optional[int] = Field(default=None, primary_key=True)
owner_id: int = Field(foreign_key='user.id', nullable=False)
name: str = Field(max_length=100, nullable=False, unique=True)
description: str = Field(max_length=500, nullable=False)
created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
status: ShopStatus = Field(sa_column=Column(Enum(ShopStatus)))
logo: Optional[str] = Field(max_length=100)
contact_email: str = Field(max_length=128, nullable=False, unique=True)
phone_number: str = Field(max_length=15, nullable=False)
address: str = Field(nullable=False)
currency: str = Field(max_length=3, nullable=False)
business_hours: ShopBusinessHours = Field(
sa_column=Column(JSONB, nullable=False, default=lambda: {})
)
links: ShopLinks = Field(
sa_column=Column(JSONB, nullable=False, default=lambda: {})
)
owner: Optional["User"] = Relationship(back_populates='owned_shops')
registered_users: List["User"] = Relationship(back_populates='registered_shop')
__table_args__ = (
CheckConstraint("business_hours ? 'hours'", name="check_business_hours_keys"),
CheckConstraint("links ? 'links'", name="check_links_keys"),
)
__all__ = ["Shop", "ShopBusinessHours", "ShopLinks", "ShopStatus"]
+59
View File
@@ -0,0 +1,59 @@
from typing import Optional, List
from uuid import UUID
from datetime import datetime
from sqlmodel import SQLModel, Field, Relationship
class User(SQLModel, table=True):
__tablename__ = 'user'
id: int = Field(primary_key=True)
uuid: UUID = Field(nullable=False, unique=True)
user_role_id: int = Field(foreign_key="user_role.id", nullable=False)
shop_id: Optional[int] = Field(foreign_key="shop.id")
username: str = Field(max_length=64, nullable=False, unique=True)
email: str = Field(max_length=128, nullable=False, unique=True)
password: str = Field(max_length=60, nullable=False)
first_name: Optional[str] = Field(max_length=64)
last_name: Optional[str] = Field(max_length=64)
phone_number: str = Field(max_length=15, nullable=False)
profile_picture: Optional[str] = Field(max_length=100)
created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
last_login: Optional[datetime] = Field(default_factory=datetime.utcnow)
owned_shops: List["Shop"] = Relationship(back_populates="owner")
registered_shop: List["Shop"] = Relationship(back_populates="registered_users")
role: Optional["UserRole"] = Relationship(back_populates="users")
preferences: Optional["UserPreferences"] = Relationship(back_populates="user")
statistics: Optional["UserStatistics"] = Relationship(back_populates="user_statistics")
class UserPreferences(SQLModel, table=True):
__tablename__ = 'user_preferences'
user_id: int = Field(foreign_key="user.id", primary_key=True)
user: Optional["User"] = Relationship(back_populates="preferences")
class UserRole(SQLModel, table=True):
__tablename__ = 'user_role'
id: int = Field(primary_key=True)
name: str = Field(nullable=False, unique=True, max_length=50)
users = Relationship(back_populates="role")
class UserStatistics(SQLModel, table=True):
__tablename__ = "user_statistics"
user_id: int = Field(foreign_key="user.id", primary_key=True)
total_spend: Optional[float] = Field(default=None)
user: Optional["User"] = Relationship(back_populates="statistics")
__all__ = ["User", "UserPreferences", "UserRole"]
Whitespace-only changes.