data model change and some new services

This commit is contained in:
2025-02-04 00:01:34 -05:00
parent 37a5dac06a
commit 85510a4671
15 changed files with 998 additions and 130 deletions

View File

@@ -1,100 +1,135 @@
from db.models import ManaboxExportData, Box, UploadHistory
from db.models import Box, File, StagedFileProduct, Product, OpenBoxCard, OpenBox, Inventory
from db.utils import db_transaction
import uuid
from uuid import uuid4 as uuid
from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy.engine.result import Row
from schemas.box import CreateOpenBoxResponse, CreateSealedBoxResponse, CreateBoxResponse
import logging
from typing import Any
from db.utils import db_transaction
from services.inventory import InventoryService
logger = logging.getLogger(__name__)
class BoxObject:
def __init__(
self, upload_id: str, set_name: str,
set_code: str, cost: float = None, date_purchased: datetime = None,
date_opened: datetime = None, box_id: str = None):
self.upload_id = upload_id
self.box_id = box_id if box_id else str(uuid.uuid4())
self.set_name = set_name
self.set_code = set_code
self.cost = cost
self.date_purchased = date_purchased
self.date_opened = date_opened
class BoxService:
def __init__(self, db: Session):
def __init__(self, db: Session, inventory_service: InventoryService):
self.db = db
def _validate_upload_id(self, upload_id: str):
# check if upload_history status = 'success'
if self.db.query(UploadHistory).filter(UploadHistory.upload_id == upload_id).first() is None:
raise Exception(f"Upload ID {upload_id} not found")
if self.db.query(UploadHistory).filter(UploadHistory.upload_id == upload_id).first().status != 'success':
raise Exception(f"Upload ID {upload_id} not successful")
# check if at least 1 row in manabox_export_data with upload_id
if self.db.query(ManaboxExportData).filter(ManaboxExportData.upload_id == upload_id).first() is None:
raise Exception(f"Upload ID {upload_id} has no data")
def _get_set_info(self, upload_id: str) -> list[Row[tuple[str, str]]]:
# get distinct set_name, set_code from manabox_export_data for upload_id
boxes = self.db.query(ManaboxExportData.set_name, ManaboxExportData.set_code).filter(ManaboxExportData.upload_id == upload_id).distinct().all()
if not boxes or len(boxes) == 0:
raise Exception(f"Upload ID {upload_id} has no data")
return boxes
def _update_manabox_export_data_box_id(self, box: Box):
# based on upload_id, set_name, set_code, update box_id in manabox_export_data for all rows where box id is null
with db_transaction(self.db):
self.db.query(ManaboxExportData).filter(
ManaboxExportData.upload_id == box.upload_id).filter(
ManaboxExportData.set_name == box.set_name).filter(
ManaboxExportData.set_code == box.set_code).filter(
ManaboxExportData.box_id == None).update({ManaboxExportData.box_id: box.id})
self.inventory_service = inventory_service
def convert_upload_to_boxes(self, upload_id: str):
self._validate_upload_id(upload_id)
# get distinct set_name, set_code from manabox_export_data for upload_id
box_set_info = self._get_set_info(upload_id)
created_boxes = []
# create boxes
for box in box_set_info:
box_obj = BoxObject(upload_id, set_name = box.set_name, set_code = box.set_code)
new_box = self.create_box(box_obj)
logger.info(f"Created box {new_box.id} for upload {upload_id}")
self._update_manabox_export_data_box_id(new_box)
created_boxes.append(new_box)
def validate_file_ids(self, file_ids: list[str]):
# check if all file_ids are valid
for file_id in file_ids:
if self.db.query(File).filter(File.id == file_id).first() is None:
raise Exception(f"File ID {file_id} not found")
def get_staged_product_data(self, file_ids: list[str]) -> StagedFileProduct:
staged_product_data = self.db.query(StagedFileProduct).filter(StagedFileProduct.file_id.in_(file_ids)).all()
return staged_product_data
def aggregate_staged_product_data(self, staged_product_data: list[Row]) -> dict[Product, int]:
product_data = {}
for row in staged_product_data:
product = self.db.query(Product).filter(Product.id == row.product_id).first()
if product not in product_data:
product_data[product] = 0
product_data[product] += row.quantity
return product_data
def find_product_for_box_data(self, create_box_data: dict[str, Any]) -> Product:
existing_product = self.db.query(Product).filter(
Product.name == create_box_data["name"], # TODO: needs complex enum
Product.type == "box",
Product.set_code == create_box_data["set_code"], # TODO: needs complex enum
Product.set_name == create_box_data["set_name"], # TODO: needs complex enum
Product.product_line == create_box_data["product_line"]).first()
return existing_product
def create_product_for_box(self, create_box_data: dict[str, Any]) -> Product:
product = Product(
id=str(uuid()),
name=create_box_data["name"],
type="box",
set_code=create_box_data["set_code"],
set_name=create_box_data["set_name"],
product_line=create_box_data["product_line"]
)
self.db.add(product)
return product
def create_box_db(self, product: Product, create_box_data: dict[str, Any]) -> Box:
box = Box(
product_id=product.id,
type=create_box_data["type"],
sku=create_box_data["sku"],
num_cards_expected=create_box_data["num_cards_expected"]
)
self.db.add(box)
return box
def create_open_box(self, product: Product, create_box_data: dict[str, Any]) -> OpenBox:
open_box = OpenBox(
id = str(uuid()),
product_id=product.id,
num_cards_actual=create_box_data["num_cards_actual"],
date_opened=datetime.strptime(create_box_data["date_opened"], "%Y-%m-%d")
)
self.db.add(open_box)
return open_box
def add_products_to_open_box(self, open_box: OpenBox, product_data: dict[Product, int]) -> None:
for product, quantity in product_data.items():
open_box_card = OpenBoxCard(
id=str(uuid()),
open_box_id=open_box.id,
card_id=product.id,
quantity=quantity
)
self.db.add(open_box_card)
def format_response(self, open_box: OpenBox = None, inventory: Inventory = None) -> CreateBoxResponse:
response = CreateBoxResponse(success=True)
return response
def create_box(self, create_box_data: dict[str, Any], file_ids: list[str] = None) -> CreateBoxResponse:
sealed = create_box_data["sealed"]
assert isinstance(sealed, bool)
if file_ids and not sealed:
self.validate_file_ids(file_ids)
staged_product_data = self.get_staged_product_data(file_ids)
product_data = self.aggregate_staged_product_data(staged_product_data)
elif file_ids and sealed:
raise Exception("Cannot add cards with a sealed box")
return {"status": "success", "boxes": f"{[box.id for box in created_boxes]}"}
# find product with all same box data
existing_product = self.find_product_for_box_data(create_box_data)
if existing_product:
box_product = existing_product
try:
with db_transaction(self.db):
if not existing_product:
box_product = self.create_product_for_box(create_box_data)
box = self.create_box_db(box_product, create_box_data)
if not sealed:
open_box = self.create_open_box(box_product, create_box_data)
if file_ids:
process_staged_products = self.inventory_service.process_staged_products(product_data)
self.add_products_to_open_box(open_box, product_data)
# should be the file service handling this but im about to die irl
# update file id status to processed
for file_id in file_ids:
file = self.db.query(File).filter(File.id == file_id).first()
file.status = "processed"
self.db.add(file)
return self.format_response(open_box=open_box)
elif not file_ids and sealed:
# add sealed box to inventory
inventory = self.inventory_service.add_sealed_box_to_inventory(box_product, 1)
return self.format_response(inventory=inventory)
def create_box(self, box: BoxObject):
with db_transaction(self.db):
box_record = Box(
id = box.box_id,
upload_id = box.upload_id,
set_name = box.set_name,
set_code = box.set_code,
cost = box.cost,
date_purchased = box.date_purchased,
date_opened = box.date_opened
)
self.db.add(box_record)
return box_record
def get_box(self):
pass
def delete_box(self, box_id: str):
# delete box
with db_transaction(self.db):
self.db.query(Box).filter(Box.id == box_id).delete()
# update manabox_export_data box_id to null
with db_transaction(self.db):
self.db.query(ManaboxExportData).filter(ManaboxExportData.box_id == box_id).update({ManaboxExportData.box_id: None})
return {"status": "success", "box_id": box_id}
def update_box(self):
pass
except Exception as e:
logger.error(f"Error creating box: {str(e)}")
raise e

75
services/file.py Normal file
View File

@@ -0,0 +1,75 @@
from sqlalchemy.orm import Session
from db.utils import db_transaction
from db.models import File, StagedFileProduct
from schemas.file import FileMetadata, FileUploadResponse, GetPreparedFilesResponse, FileDeleteResponse
import os
from uuid import uuid4 as uuid
import logging
logger = logging.getLogger(__name__)
class FileService:
def __init__(self, db: Session):
self.db = db
def _format_response(self, file: File) -> FileUploadResponse:
response = FileUploadResponse(
id = file.id,
filename = file.filename,
type = file.type,
source = file.source,
status = file.status,
service = file.service,
file_size_kb = file.filesize_kb,
date_created=file.date_created,
date_modified=file.date_modified
)
return response
def upload_file(self, content: bytes, filename: str, metadata: FileMetadata) -> FileUploadResponse:
# Save file to database
with db_transaction(self.db):
file = File(
id = str(uuid()),
filename = filename,
filepath = os.getcwd() + '/temp/' + filename, # TODO: config variable
type = metadata.type,
source = metadata.source,
filesize_kb = round(len(content) / 1024,2),
status = 'pending',
service = metadata.service
)
self.db.add(file)
# save file
with open(file.filepath, 'wb') as f:
f.write(content)
response = self._format_response(file)
return response
def get_file(self, file_id: str) -> File:
return self.db.query(File).filter(File.id == file_id).first()
def get_prepared_files(self) -> list[FileUploadResponse]:
files = self.db.query(File).filter(File.status == 'prepared').all()
if len(files) == 0:
raise Exception("No prepared files found")
result = [self._format_response(file) for file in files]
logger.debug(f"Prepared files: {result}")
response = GetPreparedFilesResponse(files=result)
return response
def get_staged_products(self, file_id: str) -> list[StagedFileProduct]:
return self.db.query(StagedFileProduct).filter(StagedFileProduct.file_id == file_id).all()
def delete_file(self, file_id: str) -> FileDeleteResponse:
file = self.get_file(file_id)
staged_products = self.get_staged_products(file_id)
if file:
with db_transaction(self.db):
self.db.delete(file)
for staged_product in staged_products:
self.db.delete(staged_product)
return {"id": file_id, "status": "deleted"}
else:
raise Exception(f"File with id {file_id} not found")

28
services/inventory.py Normal file
View File

@@ -0,0 +1,28 @@
from sqlalchemy.orm import Session
from db.models import Product, Inventory
from schemas.inventory import UpdateInventoryResponse
from db.utils import db_transaction
class InventoryService:
def __init__(self, db: Session):
self.db = db
def add_inventory(self, product: Product, quantity: int) -> Inventory:
inventory = self.db.query(Inventory).filter(Inventory.product_id == product.id).first()
if inventory is None:
inventory = Inventory(product_id=product.id, quantity=quantity)
self.db.add(inventory)
else:
inventory.quantity += quantity
return inventory
def process_staged_products(self, product_data: dict[Product, int]) -> UpdateInventoryResponse:
with db_transaction(self.db):
for product, quantity in product_data.items():
self.add_inventory(product, quantity)
return UpdateInventoryResponse(success=True)
def add_sealed_box_to_inventory(self, product: Product, quantity: int) -> UpdateInventoryResponse:
with db_transaction(self.db):
inventory = self.add_inventory(product, quantity)
return UpdateInventoryResponse(success=True)

100
services/old_box.py Normal file
View File

@@ -0,0 +1,100 @@
from db.models import ManaboxExportData, Box, UploadHistory
from db.utils import db_transaction
import uuid
from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy.engine.result import Row
import logging
logger = logging.getLogger(__name__)
class BoxObject:
def __init__(
self, upload_id: str, set_name: str,
set_code: str, cost: float = None, date_purchased: datetime = None,
date_opened: datetime = None, box_id: str = None):
self.upload_id = upload_id
self.box_id = box_id if box_id else str(uuid.uuid4())
self.set_name = set_name
self.set_code = set_code
self.cost = cost
self.date_purchased = date_purchased
self.date_opened = date_opened
class BoxService:
def __init__(self, db: Session):
self.db = db
def _validate_upload_id(self, upload_id: str):
# check if upload_history status = 'success'
if self.db.query(UploadHistory).filter(UploadHistory.upload_id == upload_id).first() is None:
raise Exception(f"Upload ID {upload_id} not found")
if self.db.query(UploadHistory).filter(UploadHistory.upload_id == upload_id).first().status != 'success':
raise Exception(f"Upload ID {upload_id} not successful")
# check if at least 1 row in manabox_export_data with upload_id
if self.db.query(ManaboxExportData).filter(ManaboxExportData.upload_id == upload_id).first() is None:
raise Exception(f"Upload ID {upload_id} has no data")
def _get_set_info(self, upload_id: str) -> list[Row[tuple[str, str]]]:
# get distinct set_name, set_code from manabox_export_data for upload_id
boxes = self.db.query(ManaboxExportData.set_name, ManaboxExportData.set_code).filter(ManaboxExportData.upload_id == upload_id).distinct().all()
if not boxes or len(boxes) == 0:
raise Exception(f"Upload ID {upload_id} has no data")
return boxes
def _update_manabox_export_data_box_id(self, box: Box):
# based on upload_id, set_name, set_code, update box_id in manabox_export_data for all rows where box id is null
with db_transaction(self.db):
self.db.query(ManaboxExportData).filter(
ManaboxExportData.upload_id == box.upload_id).filter(
ManaboxExportData.set_name == box.set_name).filter(
ManaboxExportData.set_code == box.set_code).filter(
ManaboxExportData.box_id == None).update({ManaboxExportData.box_id: box.id})
def convert_upload_to_boxes(self, upload_id: str):
self._validate_upload_id(upload_id)
# get distinct set_name, set_code from manabox_export_data for upload_id
box_set_info = self._get_set_info(upload_id)
created_boxes = []
# create boxes
for box in box_set_info:
box_obj = BoxObject(upload_id, set_name = box.set_name, set_code = box.set_code)
new_box = self.create_box(box_obj)
logger.info(f"Created box {new_box.id} for upload {upload_id}")
self._update_manabox_export_data_box_id(new_box)
created_boxes.append(new_box)
return {"status": "success", "boxes": f"{[box.id for box in created_boxes]}"}
def create_box(self, box: BoxObject):
with db_transaction(self.db):
box_record = Box(
id = box.box_id,
upload_id = box.upload_id,
set_name = box.set_name,
set_code = box.set_code,
cost = box.cost,
date_purchased = box.date_purchased,
date_opened = box.date_opened
)
self.db.add(box_record)
return box_record
def get_box(self):
pass
def delete_box(self, box_id: str):
# delete box
with db_transaction(self.db):
self.db.query(Box).filter(Box.id == box_id).delete()
# update manabox_export_data box_id to null
with db_transaction(self.db):
self.db.query(ManaboxExportData).filter(ManaboxExportData.box_id == box_id).update({ManaboxExportData.box_id: None})
return {"status": "success", "box_id": box_id}
def update_box(self):
pass

136
services/product.py Normal file
View File

@@ -0,0 +1,136 @@
from sqlalchemy.orm import Session
from db.utils import db_transaction
from db.models import Product, File, CardManabox, Card, StagedFileProduct
from io import StringIO
import pandas as pd
from services.file import FileService
from uuid import uuid4 as uuid
import logging
logger = logging.getLogger(__name__)
class ManaboxRow:
def __init__(self, row: pd.Series):
self.name = row['name']
self.set_code = row['set_code']
self.set_name = row['set_name']
self.collector_number = row['collector_number']
self.foil = row['foil']
self.rarity = row['rarity']
self.manabox_id = row['manabox_id']
self.scryfall_id = row['scryfall_id']
self.condition = row['condition']
self.language = row['language']
self.quantity = row['quantity']
class ProductService:
def __init__(self, db: Session, file_service: FileService):
self.db = db
self.file_service = file_service
def _format_manabox_df(self, df: pd.DataFrame) -> pd.DataFrame:
# format columns
df.columns = df.columns.str.lower()
df.columns = df.columns.str.replace(' ', '_')
return df
def _manabox_file_to_df(self, file: File) -> pd.DataFrame:
with open(file.filepath, 'rb') as f:
content = f.read()
content = content.decode('utf-8')
df = pd.read_csv(StringIO(content))
df = self._format_manabox_df(df)
return df
def create_staged_product(self, file: File, card_manabox:CardManabox, row: ManaboxRow) -> StagedFileProduct:
staged_product = StagedFileProduct(
id = str(uuid()),
file_id = file.id,
product_id = card_manabox.product_id,
quantity = row.quantity
)
with db_transaction(self.db):
self.db.add(staged_product)
return staged_product
def create_card_manabox(self, manabox_row: ManaboxRow) -> CardManabox:
card_manabox = CardManabox(
product_id = str(uuid()),
name = manabox_row.name,
set_code = manabox_row.set_code,
set_name = manabox_row.set_name,
collector_number = manabox_row.collector_number,
foil = manabox_row.foil,
rarity = manabox_row.rarity,
manabox_id = manabox_row.manabox_id,
scryfall_id = manabox_row.scryfall_id,
condition = manabox_row.condition,
language = manabox_row.language
)
return card_manabox
def create_product(self, card_manabox: CardManabox) -> Product:
product = Product(
id = card_manabox.product_id,
name = card_manabox.name,
set_code = card_manabox.set_code,
set_name = card_manabox.set_name,
type = 'card',
product_line = 'mtg'
)
return product
def create_card(self, card_manabox: CardManabox) -> Card:
card = Card(
product_id = card_manabox.product_id,
number = card_manabox.collector_number,
foil = card_manabox.foil,
rarity = card_manabox.rarity,
condition = card_manabox.condition,
language = card_manabox.language,
scryfall_id = card_manabox.scryfall_id,
manabox_id = card_manabox.manabox_id
)
return card
def card_manabox_lookup_create_if_not_exist(self, manabox_row: ManaboxRow) -> CardManabox:
# query based on all fields in manabox_row
card_manabox = self.db.query(CardManabox).filter(
CardManabox.name == manabox_row.name,
CardManabox.set_code == manabox_row.set_code,
CardManabox.set_name == manabox_row.set_name,
CardManabox.collector_number == manabox_row.collector_number,
CardManabox.foil == manabox_row.foil,
CardManabox.rarity == manabox_row.rarity,
CardManabox.manabox_id == manabox_row.manabox_id,
CardManabox.scryfall_id == manabox_row.scryfall_id,
CardManabox.condition == manabox_row.condition,
CardManabox.language == manabox_row.language
).first()
if not card_manabox:
# create new card_manabox, card, and product
with db_transaction(self.db):
card_manabox = self.create_card_manabox(manabox_row)
product = self.create_product(card_manabox)
card = self.create_card(card_manabox)
self.db.add(card_manabox)
self.db.add(product)
self.db.add(card)
return card_manabox
def bg_process_manabox_file(self, file_id: str):
try:
file = self.file_service.get_file(file_id)
df = self._manabox_file_to_df(file)
for index, row in df.iterrows():
manabox_row = ManaboxRow(row)
card_manabox = self.card_manabox_lookup_create_if_not_exist(manabox_row)
staged_product = self.create_staged_product(file, card_manabox, row)
# update file status
with db_transaction(self.db):
file.status = 'prepared'
except Exception as e:
with db_transaction(self.db):
file.status = 'error'
raise e

View File

@@ -41,12 +41,14 @@ class UploadService:
return df
def _create_file_upload_record(self, upload_id: str, filename: str) -> UploadHistory:
def _create_file_upload_record(self, upload_id: str, filename: str, file_size_kb: float, num_rows: int) -> UploadHistory:
file_upload_record = UploadHistory(
id = str(uuid.uuid4()),
upload_id = upload_id,
filename = filename,
status = "pending"
status = "pending",
file_size_kb = file_size_kb,
num_rows = num_rows
)
self.db.add(file_upload_record)
return file_upload_record
@@ -80,13 +82,14 @@ class UploadService:
return False
return True
def process_manabox_upload(self, content: bytes, filename: str):
def process_manabox_upload(self, content: bytes, filename: str, file_size_kb: float) -> dict:
upload = UploadObject(content=content, filename=filename)
upload.upload_id = self._create_upload_id()
upload.df = self._prepare_manabox_df(upload.content, upload.upload_id)
num_rows = len(upload.df)
with db_transaction(self.db):
file_upload_record = self._create_file_upload_record(upload.upload_id, upload.filename)
file_upload_record = self._create_file_upload_record(upload.upload_id, upload.filename, file_size_kb, num_rows)
if not self._update_manabox_data(upload.df):
# set upload to failed
file_upload_record.status = "failed"