Skip to content

Commit

Permalink
Add Database connection (#8)
Browse files Browse the repository at this point in the history
* Initial work with database

* Update README.md

* Create router folder

* Delete events route

* Add GitLens to devcontainer

* Add more info about ERD

* Add response models

* Improve loading ENV variable CORS_ORIGINS

* Update ERD & schemas

* Reformat files for Ruff

* Update database Base model

* Update imports in main.py

* Remove crud.py

* Add database relationships!

* TicketStatusEnum from models

* Add SQLite3 Editor vscode extension

* Split schemas to files

* Fix Ruff warning

* Update Ticket model

* Update and add some routers

* Add delete routes

* Add Tickets PATCHing

* Fix deprecated method of datetime

* Manually merge `main.py`

* Remove version from docker compose config file

* Add mariadb support

* Update vscode launch config

* Add Event update route

* Add libmariadb-dev installation to Ruff GitHub actions
  • Loading branch information
lukynmatuska authored Sep 15, 2024
1 parent 5f2153e commit 24bcd12
Show file tree
Hide file tree
Showing 21 changed files with 786 additions and 19 deletions.
9 changes: 7 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "pip3 install --user -r requirements.txt",
"containerEnv": {
"CORS_ORIGINS": "[\"*\"]"
},
// Configure tool-specific properties.
"customizations": {
"vscode": {
Expand All @@ -25,10 +28,12 @@
"ms-python.black-formatter",
"charliermarsh.ruff",
"DavidAnson.vscode-markdownlint",
"GitHub.vscode-github-actions"
"GitHub.vscode-github-actions",
"eamodio.gitlens",
"yy0931.vscode-sqlite3-editor"
]
}
}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
}
1 change: 1 addition & 0 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
sudo apt install -y libmariadb-dev
pip install -r requirements.txt
- name: Analysing the code with ruff
run: |
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,5 @@ cython_debug/
# Docker compose production files
docker-compose.yml
docker-compose.yaml
db/sql_lite.db
mysql/
23 changes: 23 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: FastAPI",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": [
"app.main:app",
"--reload"
],
"jinja": true,
"justMyCode": true,
"env": {
"SQLALCHEMY_DATABASE_URL": "sqlite:///./db/sql_lite.db"
}
}
]
}
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Cute Tickets Information System

## ERD

Made in [Umbrello](https://uml.sourceforge.io/)

## Production

### Instalation
Expand All @@ -12,6 +16,9 @@ Copy Docker Compose sample configuration file \
Edit Docker Compose configuration file \
`vim docker-compose.yml`

Create folder for SQL Lite database \
`mkdir db`

### Running

Run Docker Compose with logs printed (Close with Ctrl+C) \
Expand Down
164 changes: 164 additions & 0 deletions app/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
from sqlalchemy import create_engine # , MetaData
from sqlalchemy.orm import sessionmaker, DeclarativeBase, Session # , Mapped
from typing import Any
import os

SQLALCHEMY_DATABASE_URL = os.getenv("SQLALCHEMY_DATABASE_URL")
if SQLALCHEMY_DATABASE_URL is None:
raise ValueError("$SQLALCHEMY_DATABASE_URL is not defined")

if "sqlite" in SQLALCHEMY_DATABASE_URL:
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={
"check_same_thread": False
}, # ...is needed only for SQLite. It's not needed for other databases.
)
else:
engine = create_engine(SQLALCHEMY_DATABASE_URL)

SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
)


# class Base(DeclarativeBase):
# # metadata = MetaData(schema="public")
# pass


class BaseModelMixin(DeclarativeBase):
"""
Base repository class that wraps CRUD methods plus some other useful stuff.
"""

__abstract__ = True # This tells SQLAlchemy not to create a table for this class
# id: Mapped[int]

@classmethod
def get_by_id(cls, id: int, db_session: Session):
"""
@brief Gets an object by identifier
@param id The identifier
@param session The session
@return The object by identifier or None if not found.
"""
obj = db_session.get(cls, id)
return obj

@classmethod
def get_all(cls, db_session: Session) -> list:
"""
@brief Gets all objects
@param session The session
@return All objects
"""
try:
return db_session.query(cls).order_by(cls.id).all()
except Exception:
return db_session.query(cls).all()

@classmethod
def get_limit(cls, db_session: Session, limit: int = 100) -> list:
"""
@brief Gets all objects
@param session The session
@return All objects
"""
try:
return db_session.query(cls).limit(limit).order_by(cls.id).all()
except Exception:
return db_session.query(cls).limit(limit).all()

@classmethod
def get_count(cls, db_session: Session) -> int:
"""
@brief Gets the count of objects
@param session The session
@return The count of objects
"""
return db_session.query(cls).count()

@classmethod
def exists(cls, id: int, db_session: Session) -> bool:
"""
@brief Determines if the given object exists.
@param id The identifier.
@param session The session
@return True if it exists, false if not
"""
return bool(db_session.query(cls).filter_by(id=id).count())

@classmethod
def exists_cls(cls, db_session: Session) -> bool:
"""
@brief Determines if the given object exists
@param session The session
@return True if it exists, false if not
"""
return bool(db_session.query(cls).count())

@classmethod
def create(cls, db_session: Session, **kwargs):
"""
@brief Creates an object
@param session database session
@param kwargs arguments
@return The new object
"""
kwargs.pop("_sa_instance_state", None)
obj = cls(**kwargs)
db_session.add(obj)
db_session.commit()
db_session.refresh(obj)
return obj

@classmethod
def update(cls, db_session: Session, id: int, **kwargs):
"""
@brief Updates the given object
@param session database session
@param id identifier
@param kwargs arguments
@return object if it succeeds, None if it fails
"""
obj = cls.get_by_id(id, db_session)
if obj is None:
return None

for key, value in kwargs.items():
setattr(obj, key, value)
db_session.commit()
db_session.refresh(obj)
return obj

@classmethod
def delete(cls, db_session: Session, id: int):
"""
@brief Deletes the given object
@param session database session
@param id identifier
@return object if it succeeds, None if it fails
"""
obj = cls.get_by_id(id, db_session)
if obj is None:
return None
db_session.delete(obj)
db_session.commit()
return obj

@classmethod
def get_by_param(cls, db_session: Session, param_name: str, param_value: Any):
"""
@brief Gets an object by identifier
@warning This method might not work
@param db session The session
@param param_name Name of the parameter to search
@param param_value Value of the parameter to search
@return The object by identifier or None if not found
"""
column = getattr(cls, param_name, None)
if column is None:
return None
obj = db_session.query(cls).filter(column == param_value).first()
return obj
26 changes: 21 additions & 5 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
from datetime import datetime
import os
import json
import sys
from app.features.git import Git
from app.routers import events, ticket_groups, tickets
from app.schemas.root import RootResponse
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .features.git import Git


app = FastAPI(
swagger_ui_parameters={
Expand All @@ -16,7 +20,14 @@
description="REST API with database of ticket reservations",
)

origins = json.loads(os.getenv("CORS_ORIGINS"))
try:
origins = json.loads(os.getenv("CORS_ORIGINS"))
except: # noqa: E722
print(
'Missing defined ENV variable CORS_ORIGINS, using default value ["*"].',
file=sys.stderr,
)
origins = ["*"]

app.add_middleware(
CORSMiddleware,
Expand All @@ -27,8 +38,8 @@
)


@app.get("/")
@app.head("/")
@app.get("/", response_model=RootResponse)
@app.head("/", response_model=RootResponse)
async def root():
"""Root path method"""
git = Git()
Expand All @@ -39,7 +50,12 @@ async def root():
}


@app.get("/health-check")
@app.get("/health-check", response_model=str)
def health_check():
"""Method for docker container health check"""
return "success"


app.include_router(events.router)
app.include_router(ticket_groups.router)
app.include_router(tickets.router)
57 changes: 57 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from sqlalchemy import DateTime, Integer, String, ForeignKey, Enum
from sqlalchemy.orm import Mapped, relationship, mapped_column
import enum
from app.database import BaseModelMixin


class TicketStatusEnum(enum.Enum):
new = 0
confirmed = 1
paid = 2
cancelled = 3


class Ticket(BaseModelMixin):
__tablename__ = "tickets"

id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
email: Mapped[str] = mapped_column(String(length=250))
firstname: Mapped[str] = mapped_column(String(length=250))
lastname: Mapped[str] = mapped_column(String(length=250))
order_date: Mapped[DateTime] = mapped_column(DateTime)
status: Mapped[TicketStatusEnum] = mapped_column(Enum(TicketStatusEnum))
description: Mapped[str] = mapped_column(String(length=250), default="")
# maybe there should be an attribute for ticket cancellation

# Relationships
group_id: Mapped[int] = mapped_column(
ForeignKey("ticket_groups.id", ondelete="CASCADE")
)
group = relationship("TicketGroup", back_populates="tickets")


class TicketGroup(BaseModelMixin):
__tablename__ = "ticket_groups"

id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(length=250))
capacity: Mapped[int] = mapped_column(Integer)

# Relationships
event_id: Mapped[int] = mapped_column(ForeignKey("events.id", ondelete="CASCADE"))
tickets = relationship("Ticket", back_populates="group", passive_deletes=True)
event = relationship("Event", back_populates="ticket_groups")


class Event(BaseModelMixin):
__tablename__ = "events"

id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(length=250))
tickets_sales_start: Mapped[DateTime] = mapped_column(DateTime)
tickets_sales_end: Mapped[DateTime] = mapped_column(DateTime)

# Relationships
ticket_groups = relationship(
"TicketGroup", back_populates="event", passive_deletes=True
)
Empty file added app/routers/__init__.py
Empty file.
Loading

0 comments on commit 24bcd12

Please sign in to comment.