[rewrite] Major changes to code - switched to postgres, added initial backend docker files
This commit is contained in:
1 parent
c7e20fc935
commit
71e916586e
52 files changed
+783
-537
No files matched your search
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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"]
|
||||
@@ -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.
Reference in new issue
Block a user