[rewrite] Updated models a bit, preparing to make first functional routes

This commit is contained in:
Thastertyn 2025-02-23 15:56:39 +01:00
parent 414b84a2d4
commit f0c378b20f
21 changed files with 203 additions and 90 deletions

View File

@ -1,29 +0,0 @@
# from flask import Flask
# from flask_jwt_extended import JWTManager
# from flask_mail import Mail
# from flasgger import Swagger
# from app.doc.main_swag import main_swagger
# app = Flask(__name__)
# from app.config import FlaskTesting, FlaskProduction
# app.config.from_object(FlaskTesting)
# flask_mail = Mail(app)
# jwt_manager = JWTManager(app)
# swag = Swagger(app, template=main_swagger)
# def create_app():
# from app.api import bp, bp_errors, bp_product, bp_user, bp_cart
# app.register_blueprint(bp)
# app.register_blueprint(bp_errors)
# app.register_blueprint(bp_product)
# app.register_blueprint(bp_user)
# app.register_blueprint(bp_cart)
# from . import jwt_utils
# return app

View File

View File

@ -0,0 +1,20 @@
class DatabaseError(Exception):
# Inspired by OSError which also uses errno's
# It's a better approach than using a class for each error
UNKNOWN_ERROR = -1
CONNECTION_ERROR = 1
EMPTY_CONFIG = 2
DUPLICATE_ENTRY = 3
def __init__(self, message: str, errno: int, **kwargs):
super().__init__(message)
self.message = message
self.errno = errno
for key, value in kwargs.items():
setattr(self, key, value)
__all__ = ["DatabaseError"]

View File

@ -0,0 +1,68 @@
import logging
from typing import Generator
from contextlib import contextmanager
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy import create_engine, text
from sqlalchemy.exc import DatabaseError as SqlAlchemyDatabaseError
from app.database.exceptions import DatabaseError
from app.models.base_model import Base
class DatabaseManager():
_instance: 'DatabaseManager' = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self) -> None:
if hasattr(self, "engine"):
return
self.logger = logging.getLogger(__name__)
self.logger.info("Initializing Database")
self.engine = create_engine('sqlite:///bank.db')
self.Session = sessionmaker(bind=self.engine)
self.create_tables()
def create_tables(self):
self.logger.debug("Creating tables")
Base.metadata.create_all(self.engine)
def cleanup(self) -> None:
self.engine.dispose()
def test_connection(self) -> bool:
self.logger.debug("Testing database connection")
try:
with self.engine.connect() as connection:
connection.execute(text("select 1"))
self.logger.debug("Database connection successful")
return True
except SqlAlchemyDatabaseError as e:
self.logger.critical("Database connection failed: %s", e)
raise DatabaseError("Database connection failed", DatabaseError.CONNECTION_ERROR) from e
return False
@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()
__all__ = ["DatabaseManager"]

View File

@ -0,0 +1,36 @@
from typing import Annotated
from pydantic import BaseModel
from fastapi import Header, HTTPException, Request, Body
async def get_token_header(x_token: Annotated[str, Header()]):
if x_token != "fake-super-secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
class LoginRequest(BaseModel):
username: str
password: str
async def get_jsession_id(request: Request):
jsessionid = (
request.headers.get("JSESSIONID")
or request.cookies.get("JSESSIONID")
or request.query_params.get("jsessionid")
)
if not jsessionid:
raise HTTPException(status_code=400, detail="JSESSIONID is required for this operation")
return jsessionid
async def get_credentials(credentials: LoginRequest = Body(...)):
return credentials.dict()
async def get_query_token(token: str):
if token != "jessica":
raise HTTPException(status_code=400, detail="No Jessica token provided")

View File

@ -1,21 +0,0 @@
import mysql.connector
import redis
import os
from app.config import RedisConfig
from app.config import MySqlConfig
db_connection = mysql.connector.connect(
host=MySqlConfig.MYSQL_HOST,
user=MySqlConfig.MYSQL_USER,
password=MySqlConfig.MYSQL_PASSWORD,
database=MySqlConfig.MYSQL_DATABASE,
)
jwt_redis_blocklist = redis.StrictRedis(
host=RedisConfig.REDIS_HOST,
port=RedisConfig.REDIS_PORT,
password=RedisConfig.REDIS_PASSWORD,
db=0,
decode_responses=True,
)

View File

@ -1,5 +0,0 @@
from .user_model import *
__all__ = [
*user_model.__all__
]

View File

@ -22,5 +22,9 @@ class Shop(Base):
business_hours = Column(JSON, nullable=False)
links = Column(JSON, nullable=False)
users = relationship('User', back_populates='shop')
products = relationship('Product', back_populates='shop')
owner = relationship('User', back_populates='owned_shops')
registered_users = relationship('User', back_populates='registered_shop')
# products = relationship('Product', back_populates='shop')
__all__ = ["Shop"]

View File

@ -1,33 +1,32 @@
from sqlalchemy import ForeignKey, Column, Integer, JSON, TIMESTAMP, String, Enum
from sqlalchemy.sql import func
from sqlalchemy import Column, String, Integer, ForeignKey, TIMESTAMP
from sqlalchemy.dialects.mysql import INTEGER
from sqlalchemy.orm import relationship
from .base_model import Base
class User(Base):
__tablename__ = "user"
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
shop_id = Column(Integer, ForeignKey("shop.id"), nullable=True)
username = Column(String(64), unique=True, nullable=False)
email = Column(String(128), unique=True, nullable=False)
id = Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True)
user_role_id = Column(INTEGER(unsigned=True), ForeignKey('user_role.id'), nullable=False)
shop_id = Column(Integer, ForeignKey('shop.id'), nullable=True)
username = Column(String(64), nullable=False, unique=True)
email = Column(String(128), nullable=False, unique=True)
password = Column(String(60), nullable=False)
role = Column(Enum("customer", "employee", "manager", "owner", "admin", name="user_role"), nullable=False, default="customer")
first_name = Column(String(64), nullable=True)
last_name = Column(String(64), nullable=True)
phone_number = Column(String(15), nullable=True)
created_at = Column(TIMESTAMP, default=func.now, nullable=True)
updated_at = Column(TIMESTAMP, default=func.now, onupdate=func.now, nullable=True)
last_login = Column(TIMESTAMP, nullable=True)
phone_number = Column(String(15), nullable=False)
profile_picture = Column(String(100), nullable=True)
preferences = Column(JSON, nullable=True)
created_at = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP")
updated_at = Column(TIMESTAMP, nullable=False, server_default="CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
last_login = Column(TIMESTAMP, nullable=True, server_default="CURRENT_TIMESTAMP")
shop = relationship("Shop", back_populates="users")
carts = relationship("Cart", back_populates="user")
purchases = relationship("Purchase", back_populates="user")
statistics = relationship("UserStatistics", back_populates="user")
wishlists = relationship("Wishlist", back_populates="user")
owned_shops = relationship("Shop", back_populates="owner")
registered_shop = relationship("Shop", back_populates="registered_users")
role = relationship("UserRole", back_populates="users")
preferences = relationship("UserPreferences", uselist=False, back_populates="user")
statistics = relationship("UserStatistics", uselist=False, back_populates="user_statistics")
__all__ = ["User"]

View File

@ -0,0 +1,11 @@
from sqlalchemy import Column, ForeignKey
from sqlalchemy.dialects.mysql import INTEGER
from sqlalchemy.orm import relationship
from .base_model import Base
class UserPreferences(Base):
__tablename__ = 'user_preferences'
user_id = Column(INTEGER(unsigned=True), ForeignKey('user.id'), primary_key=True)
user = relationship("User", back_populates="preferences")

View File

@ -0,0 +1,16 @@
from sqlalchemy import Column, String
from sqlalchemy.dialects.mysql import INTEGER
from sqlalchemy.orm import relationship
from .base_model import Base
class UserRole(Base):
__tablename__ = 'user_role'
id = Column(INTEGER(unsigned=True), primary_key=True, autoincrement=True)
name = Column(String(45), nullable=False, unique=True)
users = relationship("User", back_populates="role")
__all__ = ["UserRole"]

View File

@ -1,11 +1,12 @@
from sqlalchemy import Column, Integer, Float, ForeignKey
from sqlalchemy import Column, Float, ForeignKey
from sqlalchemy.dialects.mysql import INTEGER
from sqlalchemy.orm import relationship
from .base_model import Base
class UserStatistics(Base):
__tablename__ = "user_statistics"
user_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
user_id = Column(INTEGER(unsigned=True), ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
total_spend = Column(Float, nullable=True)
user = relationship("User", back_populates="user_statistics", foreign_keys=[user_id])

View File

@ -0,0 +1,13 @@
from functools import wraps
from sqlalchemy.exc import DatabaseError as SqlAlchemyDatabaseError
from app.database.exceptions import DatabaseError
def handle_database_errors(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except SqlAlchemyDatabaseError as e:
raise DatabaseError(str(e), -1) from e
return wrapper

12
backend/poetry.lock generated
View File

@ -204,6 +204,16 @@ files = [
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "mysql-connector"
version = "2.2.9"
description = "MySQL driver written in Python"
optional = false
python-versions = "*"
files = [
{file = "mysql-connector-2.2.9.tar.gz", hash = "sha256:1733e6ce52a049243de3264f1fbc22a852cb35458c4ad739ba88189285efdf32"},
]
[[package]]
name = "nodeenv"
version = "1.9.1"
@ -614,4 +624,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "250084c97cedad74f83b3a3420c71f338eb85d309bf30aed8e84743a3c2b26e3"
content-hash = "96176c45f5a989a912f901370d6680231d9be0ba6a6e12a753699f17bed8ca93"

View File

@ -11,6 +11,7 @@ python = "^3.12"
fastapi = "^0.115.6"
sqlalchemy = "^2.0.37"
python-dotenv = "^1.0.1"
mysql-connector = "^2.2.9"
[tool.poetry.group.dev.dependencies]

11
main.py
View File

@ -1,11 +0,0 @@
from dotenv import load_dotenv
from app import create_app
load_dotenv()
app = create_app()
if __name__ == "__main__":
print("Hello, Flask")
app.run(use_reloader=False)