data model change and some new services
This commit is contained in:
parent
37a5dac06a
commit
85510a4671
1
.gitignore
vendored
1
.gitignore
vendored
@ -170,3 +170,4 @@ cython_debug/
|
||||
|
||||
# my stuff
|
||||
*.db
|
||||
temp/
|
379
db/models.py
379
db/models.py
@ -1,31 +1,353 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import relationship, validates
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class Box(Base):
|
||||
__tablename__ = "boxes"
|
||||
## Core Models
|
||||
|
||||
id = Column(String, primary_key=True, index=True)
|
||||
upload_id = Column(String, ForeignKey("upload_history.upload_id"))
|
||||
class Product(Base):
|
||||
"""
|
||||
product is the concept of a physical item that can be sold
|
||||
"""
|
||||
__tablename__ = "products"
|
||||
|
||||
@validates("type")
|
||||
def validate_type(self, key, type: str):
|
||||
if type not in ProductTypeEnum or type.lower() not in ProductTypeEnum:
|
||||
raise ValueError(f"Invalid product type: {type}")
|
||||
return type
|
||||
|
||||
@validates("product_line")
|
||||
def validate_product_line(self, key, product_line: str):
|
||||
if product_line not in ProductLineEnum or product_line.lower() not in ProductLineEnum:
|
||||
raise ValueError(f"Invalid product line: {product_line}")
|
||||
return product_line
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
name = Column(String)
|
||||
type = Column(String) # box or card
|
||||
product_line = Column(String) # pokemon, mtg, etc.
|
||||
set_name = Column(String)
|
||||
set_code = Column(String)
|
||||
type = Column(String)
|
||||
cost = Column(Float)
|
||||
date_purchased = Column(DateTime)
|
||||
date_opened = Column(DateTime)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Sale(Base):
|
||||
"""
|
||||
sale represents a transaction where a product was sold to a customer on a marketplace
|
||||
"""
|
||||
__tablename__ = "sales"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
ledger_id = Column(String, ForeignKey("ledgers.id"))
|
||||
customer_id = Column(String, ForeignKey("customers.id"))
|
||||
marketplace_id = Column(String, ForeignKey("marketplaces.id"))
|
||||
amount = Column(Float)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Ledger(Base):
|
||||
"""
|
||||
ledger associates financial transactions with a user
|
||||
"""
|
||||
__tablename__ = "ledgers"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
user_id = Column(String, ForeignKey("users.id"))
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Expense(Base):
|
||||
"""
|
||||
expense is any cash outflow associated with moving a product
|
||||
can be optionally associated with a sale or a product
|
||||
"""
|
||||
__tablename__ = "expenses"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
ledger_id = Column(String, ForeignKey("ledgers.id"))
|
||||
product_id = Column(String, ForeignKey("products.id"), nullable=True)
|
||||
sale_id = Column(String, ForeignKey("sales.id"), nullable=True)
|
||||
cost = Column(Float)
|
||||
type = Column(String) # price paid, cogs, shipping, refund, supplies, subscription, fee, etc.
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Marketplace(Base):
|
||||
"""
|
||||
Marketplace represents a marketplace where products can be sold
|
||||
"""
|
||||
__tablename__ = "marketplaces"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Box(Base):
|
||||
"""
|
||||
Box Represents a physical product with a sku that contains trading cards
|
||||
Boxes can be sealed or opened
|
||||
Opened boxes have cards associated with them
|
||||
A box contains cards regardless of the inventory status of those cards
|
||||
"""
|
||||
__tablename__ = "boxes"
|
||||
|
||||
@validates("type")
|
||||
def validate_type(self, key, type: str):
|
||||
if type not in BoxTypeEnum or type.lower() not in BoxTypeEnum:
|
||||
raise ValueError(f"Invalid box type: {type}")
|
||||
return type
|
||||
|
||||
product_id = Column(String, ForeignKey("products.id"), primary_key=True)
|
||||
type = Column(String) # collector box, play box, etc.
|
||||
sku = Column(String)
|
||||
num_cards_expected = Column(Integer, nullable=True)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class OpenBox(Base):
|
||||
__tablename__ = "open_boxes"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
product_id = Column(String, ForeignKey("products.id"))
|
||||
num_cards_actual = Column(Integer)
|
||||
date_opened = Column(DateTime, default=datetime.now)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Card(Base):
|
||||
"""
|
||||
Card represents the concept of a distinct card
|
||||
Cards have metadata from different sources
|
||||
internal: box, inventory, upload
|
||||
external: price, attributes - scryfall, tcgplayer, manabox
|
||||
"""
|
||||
__tablename__ = "cards"
|
||||
|
||||
@validates("rarity")
|
||||
def validate_rarity(self, key, rarity: str):
|
||||
single_character_rarity = {'m': 'mythic', 'r': 'rare', 'u': 'uncommon', 'c': 'common', 'l': 'land', 'p': 'promo', 's': 'special'}
|
||||
if rarity not in RarityEnum:
|
||||
if rarity.lower() in RarityEnum:
|
||||
rarity = rarity.lower()
|
||||
elif rarity in single_character_rarity:
|
||||
rarity = single_character_rarity[rarity]
|
||||
else:
|
||||
raise ValueError(f"Invalid rarity: {rarity}")
|
||||
return rarity
|
||||
|
||||
@validates("condition")
|
||||
def validate_condition(self, key, condition: str):
|
||||
if condition not in ConditionEnum:
|
||||
if condition.lower() in ConditionEnum:
|
||||
condition = condition.lower()
|
||||
elif condition.lower().strip().replace(' ', '_') in ConditionEnum:
|
||||
condition = condition.lower().strip().replace(' ', '_')
|
||||
else:
|
||||
raise ValueError(f"Invalid condition: {condition}")
|
||||
return condition
|
||||
|
||||
product_id = Column(String, ForeignKey("products.id"), primary_key=True)
|
||||
number = Column(String)
|
||||
foil = Column(String)
|
||||
rarity = Column(String)
|
||||
condition = Column(String)
|
||||
language = Column(String)
|
||||
scryfall_id = Column(String)
|
||||
manabox_id = Column(String)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class CardManabox(Base):
|
||||
__tablename__ = "card_manabox"
|
||||
|
||||
product_id = Column(String, ForeignKey("cards.product_id"), primary_key=True)
|
||||
name = Column(String)
|
||||
set_code = Column(String)
|
||||
set_name = Column(String)
|
||||
collector_number = Column(String)
|
||||
foil = Column(String)
|
||||
rarity = Column(String)
|
||||
manabox_id = Column(String)
|
||||
scryfall_id = Column(String)
|
||||
condition = Column(String)
|
||||
language = Column(String)
|
||||
|
||||
class Warehouse(Base):
|
||||
"""
|
||||
container that is associated with a user and contains inventory and stock
|
||||
"""
|
||||
__tablename__ = "warehouse"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
user_id = Column(String, ForeignKey("users.id"), default="admin")
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Stock(Base):
|
||||
"""
|
||||
contains products that are listed for sale
|
||||
"""
|
||||
__tablename__ = "stock"
|
||||
|
||||
product_id = Column(String, ForeignKey("products.id"), primary_key=True)
|
||||
warehouse_id = Column(String, ForeignKey("warehouse.id"), default="default")
|
||||
marketplace_id = Column(String, ForeignKey("marketplaces.id"))
|
||||
quantity = Column(Integer)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Inventory(Base):
|
||||
"""
|
||||
contains products in inventory (not necessarily listed for sale)
|
||||
sealed product in breakdown queue, held sealed product, speculatively held singles, etc.
|
||||
inventory can contain products across multiple marketplaces
|
||||
"""
|
||||
__tablename__ = "inventory"
|
||||
|
||||
product_id = Column(String, ForeignKey("products.id"), primary_key=True)
|
||||
warehouse_id = Column(String, ForeignKey("warehouse.id"), default="default")
|
||||
quantity = Column(Integer)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class User(Base):
|
||||
"""
|
||||
User represents a user in the system
|
||||
"""
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Customer(Base):
|
||||
"""
|
||||
Customer represents a customer that has purchased at least 1 product
|
||||
"""
|
||||
__tablename__ = "customers"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class StagedFileProduct(Base):
|
||||
__tablename__ = "staged_file_products"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
product_id = Column(String, ForeignKey("products.id"))
|
||||
file_id = Column(String, ForeignKey("files.id"))
|
||||
quantity = Column(Integer)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class File(Base):
|
||||
"""
|
||||
File represents a file that has been uploaded to or retrieved by the system
|
||||
"""
|
||||
__tablename__ = "files"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
type = Column(String) # upload, export, etc.
|
||||
source = Column(String) # manabox, tcgplayer, etc.
|
||||
service = Column(String) # pricing, data, etc.
|
||||
filename = Column(String)
|
||||
filepath = Column(String) # backup location
|
||||
filesize_kb = Column(Float)
|
||||
status = Column(String)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class Price(Base):
|
||||
__tablename__ = "price"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
product_id = Column(String, ForeignKey("products.id"))
|
||||
marketplace_id = Column(String, ForeignKey("marketplaces.id"))
|
||||
type = Column(String) # market, direct, low, low_with_shipping, marketplace
|
||||
price = Column(Float)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class StorageBlock(Base):
|
||||
"""
|
||||
StorageBlock represents a physical storage location for products (50 card indexed block in a box)
|
||||
"""
|
||||
__tablename__ = "storage_blocks"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
warehouse_id = Column(String, ForeignKey("warehouse.id"))
|
||||
name = Column(String)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
## Relationships
|
||||
|
||||
class ProductBlock(Base):
|
||||
"""
|
||||
ProductBlock represents the relationship between a product and a storage block
|
||||
which products are in a block and at what index
|
||||
"""
|
||||
__tablename__ = "product_block"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
product_id = Column(String, ForeignKey("products.id"))
|
||||
block_id = Column(String, ForeignKey("storage_blocks.id"))
|
||||
block_index = Column(Integer)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class ProductMarketPrice(Base):
|
||||
__tablename__ = "product_market_price"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
product_id = Column(String, ForeignKey("products.id"))
|
||||
marketplace_id = Column(String, ForeignKey("marketplaces.id"))
|
||||
price_id = Column(String, ForeignKey("price.id"))
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class OpenBoxCard(Base):
|
||||
"""
|
||||
OpenedBoxCard represents the relationship between an opened box and the cards it contains
|
||||
"""
|
||||
__tablename__ = "open_box_card"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
open_box_id = Column(String, ForeignKey("open_boxes.id"))
|
||||
card_id = Column(String, ForeignKey("cards.product_id"))
|
||||
quantity = Column(Integer)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class ProductSale(Base):
|
||||
"""
|
||||
ProductSale represents the relationship between products and sales
|
||||
"""
|
||||
__tablename__ = "product_sale"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
product_id = Column(String, ForeignKey("products.id"))
|
||||
sale_id = Column(String, ForeignKey("sales.id"))
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
## older
|
||||
|
||||
class ManaboxExportData(Base):
|
||||
__tablename__ = "manabox_export_data"
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
upload_id = Column(String)
|
||||
box_id = Column(String, nullable=True)
|
||||
# upload_id = Column(String)
|
||||
# box_id = Column(String, nullable=True)
|
||||
name = Column(String)
|
||||
set_code = Column(String)
|
||||
set_name = Column(String)
|
||||
@ -50,6 +372,8 @@ class UploadHistory(Base):
|
||||
id = Column(String, primary_key=True)
|
||||
upload_id = Column(String)
|
||||
filename = Column(String)
|
||||
file_size_kb = Column(Float)
|
||||
num_rows = Column(Integer)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
status = Column(String)
|
||||
@ -160,3 +484,36 @@ class UnmatchedManaboxData(Base):
|
||||
reason = Column(String)
|
||||
date_created = Column(DateTime, default=datetime.now)
|
||||
date_modified = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
# enums
|
||||
|
||||
class RarityEnum(str, Enum):
|
||||
COMMON = "common"
|
||||
UNCOMMON = "uncommon"
|
||||
RARE = "rare"
|
||||
MYTHIC = "mythic"
|
||||
LAND = "land"
|
||||
PROMO = "promo"
|
||||
SPECIAL = "special"
|
||||
|
||||
class ConditionEnum(str, Enum):
|
||||
MINT = "mint"
|
||||
NEAR_MINT = "near_mint"
|
||||
LIGHTLY_PLAYED = "lightly_played"
|
||||
MODERATELY_PLAYED = "moderately_played"
|
||||
HEAVILY_PLAYED = "heavily_played"
|
||||
DAMAGED = "damaged"
|
||||
|
||||
class BoxTypeEnum(str, Enum):
|
||||
COLLECTOR = "collector"
|
||||
PLAY = "play"
|
||||
DRAFT = "draft"
|
||||
COMMANDER = "commander"
|
||||
|
||||
class ProductLineEnum(str, Enum):
|
||||
MTG = "mtg"
|
||||
POKEMON = "pokemon"
|
||||
|
||||
class ProductTypeEnum(str, Enum):
|
||||
BOX = "box"
|
||||
CARD = "card"
|
@ -4,10 +4,39 @@ from services.upload import UploadService
|
||||
from services.box import BoxService
|
||||
from services.tcgplayer import TCGPlayerService
|
||||
from services.pricing import PricingService
|
||||
from fastapi import Depends
|
||||
from services.file import FileService
|
||||
from services.product import ProductService
|
||||
from services.inventory import InventoryService
|
||||
from fastapi import Depends, Form
|
||||
from db.database import get_db
|
||||
from schemas.file import FileMetadata
|
||||
|
||||
|
||||
## file
|
||||
# file
|
||||
def get_file_service(db: Session = Depends(get_db)) -> FileService:
|
||||
"""Dependency injection for FileService"""
|
||||
return FileService(db)
|
||||
|
||||
# metadata
|
||||
def get_file_metadata(
|
||||
type: str = Form(...),
|
||||
source: str = Form(...)
|
||||
) -> FileMetadata:
|
||||
"""Dependency injection for FileMetadata"""
|
||||
return FileMetadata(type=type, source=source)
|
||||
|
||||
# product
|
||||
def get_product_service(db: Session = Depends(get_db), file_service: FileService = Depends(get_file_service)) -> ProductService:
|
||||
"""Dependency injection for ProductService"""
|
||||
return ProductService(db, file_service)
|
||||
|
||||
|
||||
## Inventory
|
||||
def get_inventory_service(db: Session = Depends(get_db)) -> InventoryService:
|
||||
"""Dependency injection for InventoryService"""
|
||||
return InventoryService(db)
|
||||
|
||||
## Upload
|
||||
|
||||
def get_upload_service(db: Session = Depends(get_db)) -> UploadService:
|
||||
@ -16,9 +45,9 @@ def get_upload_service(db: Session = Depends(get_db)) -> UploadService:
|
||||
|
||||
## box
|
||||
|
||||
def get_box_service(db: Session = Depends(get_db)) -> BoxService:
|
||||
def get_box_service(db: Session = Depends(get_db), inventory_service: InventoryService = Depends(get_inventory_service)) -> BoxService:
|
||||
"""Dependency injection for BoxService"""
|
||||
return BoxService(db)
|
||||
return BoxService(db, inventory_service)
|
||||
|
||||
## Pricing
|
||||
|
||||
|
@ -7,7 +7,11 @@ from services.upload import UploadService
|
||||
from services.box import BoxService
|
||||
from services.tcgplayer import TCGPlayerService
|
||||
from services.data import DataService
|
||||
from dependencies import get_data_service, get_upload_service, get_tcgplayer_service, get_box_service
|
||||
from services.file import FileService
|
||||
from services.product import ProductService
|
||||
from schemas.file import FileMetadata, FileUploadResponse, GetPreparedFilesResponse, FileDeleteResponse
|
||||
from schemas.box import CreateBoxResponse, CreateBoxRequestData
|
||||
from dependencies import get_data_service, get_upload_service, get_tcgplayer_service, get_box_service, get_file_metadata, get_file_service, get_product_service
|
||||
|
||||
|
||||
import logging
|
||||
@ -15,30 +19,55 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["cards"])
|
||||
|
||||
## health check
|
||||
@router.get("/health", response_model=dict)
|
||||
async def health_check() -> dict:
|
||||
"""
|
||||
Health check endpoint
|
||||
"""
|
||||
logger.info("Health check")
|
||||
return {"status": "ok"}
|
||||
|
||||
## test endpoint - logs all detail about request
|
||||
@router.post("/test", response_model=dict)
|
||||
async def test_endpoint(request: Request, file:UploadFile = File(...)) -> dict:
|
||||
"""
|
||||
Test endpoint
|
||||
"""
|
||||
# FILE
|
||||
@router.post("/file/uploadManabox", response_model=FileUploadResponse)
|
||||
async def upload_file(
|
||||
background_tasks: BackgroundTasks,
|
||||
file: UploadFile = File(...),
|
||||
file_service: FileService = Depends(get_file_service),
|
||||
product_service: ProductService = Depends(get_product_service),
|
||||
metadata: FileMetadata = Depends(get_file_metadata)) -> FileUploadResponse:
|
||||
try:
|
||||
content = await file.read()
|
||||
# log filename
|
||||
logger.info(f"file received: {file.filename}")
|
||||
# print first 100 characters of file content
|
||||
logger.info(f"file content: {content[:100]}")
|
||||
|
||||
return {"status": "ok"}
|
||||
metadata.service = 'product'
|
||||
result = file_service.upload_file(content, file.filename, metadata)
|
||||
background_tasks.add_task(product_service.bg_process_manabox_file, result.id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"File upload failed: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/file/getPreparedFiles", response_model=GetPreparedFilesResponse)
|
||||
async def get_prepared_files(file_service: FileService = Depends(get_file_service)) -> GetPreparedFilesResponse:
|
||||
try:
|
||||
response = file_service.get_prepared_files()
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Get prepared files failed: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.post("/file/deleteFile", response_model=FileDeleteResponse)
|
||||
async def delete_file(file_id: str, file_service: FileService = Depends(get_file_service)) -> FileDeleteResponse:
|
||||
try:
|
||||
response = file_service.delete_file(file_id)
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Delete file failed: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.post("/box/createBox", response_model=CreateBoxResponse)
|
||||
async def create_box(file_ids: list[str], create_box_data: CreateBoxRequestData, box_service: BoxService = Depends(get_box_service)) -> CreateBoxResponse:
|
||||
try:
|
||||
create_box_data = create_box_data.dict()
|
||||
response = box_service.create_box(create_box_data, file_ids)
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Create box failed: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
## all old below
|
||||
|
||||
@router.post("/upload/manabox", response_model=dict)
|
||||
async def upload_manabox(
|
||||
@ -55,6 +84,8 @@ async def upload_manabox(
|
||||
# Read the file content
|
||||
content = await file.read()
|
||||
filename = file.filename
|
||||
file_size = len(content)
|
||||
file_size_kb = file_size / 1024
|
||||
if not content:
|
||||
logger.error("Empty file content")
|
||||
raise HTTPException(status_code=400, detail="Empty file content")
|
||||
@ -64,7 +95,7 @@ async def upload_manabox(
|
||||
logger.error("File must be a CSV")
|
||||
raise HTTPException(status_code=400, detail="File must be a CSV")
|
||||
|
||||
result = upload_service.process_manabox_upload(content, filename)
|
||||
result = upload_service.process_manabox_upload(content, filename, file_size_kb)
|
||||
background_tasks.add_task(data_service.bg_set_manabox_tcg_relationship, upload_id=result[1])
|
||||
return result[0]
|
||||
except Exception as e:
|
||||
|
0
schemas/__init__.py
Normal file
0
schemas/__init__.py
Normal file
13
schemas/base.py
Normal file
13
schemas/base.py
Normal file
@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
# Base schemas with shared attributes
|
||||
class BaseSchema(BaseModel):
|
||||
date_created: datetime
|
||||
date_modified: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True # Allows conversion from SQLAlchemy models
|
25
schemas/box.py
Normal file
25
schemas/box.py
Normal file
@ -0,0 +1,25 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from schemas.base import BaseSchema
|
||||
|
||||
class CreateBoxResponse(BaseModel):
|
||||
success: bool = Field(..., title="Success")
|
||||
|
||||
class CreateOpenBoxResponse(CreateBoxResponse):
|
||||
open_box_id: str
|
||||
|
||||
class CreateSealedBoxResponse(CreateBoxResponse):
|
||||
product_id: str
|
||||
quantity: int
|
||||
|
||||
class CreateBoxRequestData(BaseModel):
|
||||
type: str = Field(..., title="Box Type (collector, play, draft)")
|
||||
name: str = Field(..., title="Name")
|
||||
product_line: str = Field(..., title="Product Line (MTG, Pokemon, etc)")
|
||||
set_name: str = Field(..., title="Set Name")
|
||||
set_code: str = Field(..., title="Set Code")
|
||||
sealed: bool = Field(..., title="Sealed: Boolean")
|
||||
sku: str = Field(..., title="SKU")
|
||||
num_cards_expected: int = Field(..., title="Number of cards expected")
|
||||
num_cards_actual: int = Field(None, title="Number of cards actual")
|
||||
date_purchased: str = Field(..., title="Date purchased")
|
||||
date_opened: str = Field(None, title="Date opened")
|
29
schemas/file.py
Normal file
29
schemas/file.py
Normal file
@ -0,0 +1,29 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from schemas.base import BaseSchema
|
||||
from fastapi import UploadFile
|
||||
|
||||
# For additional metadata about the upload
|
||||
class FileMetadata(BaseModel):
|
||||
source: str = Field(..., title="Source")
|
||||
type: str = Field(..., title="Type")
|
||||
# optional
|
||||
service: Optional[str] = Field(None, title="Service")
|
||||
|
||||
# For the response after upload
|
||||
class FileUploadResponse(BaseSchema):
|
||||
id: str
|
||||
filename: str
|
||||
type: str
|
||||
file_size_kb: float
|
||||
source: str
|
||||
status: str
|
||||
service: str
|
||||
|
||||
class FileDeleteResponse(BaseModel):
|
||||
id: str
|
||||
status: str
|
||||
|
||||
class GetPreparedFilesResponse(BaseModel):
|
||||
files: list[FileUploadResponse]
|
6
schemas/inventory.py
Normal file
6
schemas/inventory.py
Normal file
@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from schemas.base import BaseSchema
|
||||
|
||||
|
||||
class UpdateInventoryResponse(BaseModel):
|
||||
success: bool = Field(..., title="Success")
|
197
services/box.py
197
services/box.py
@ -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
|
||||
self.inventory_service = inventory_service
|
||||
|
||||
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 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_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 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 _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 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 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 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
|
||||
|
||||
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
|
||||
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(box_record)
|
||||
return box_record
|
||||
self.db.add(product)
|
||||
return product
|
||||
|
||||
def get_box(self):
|
||||
pass
|
||||
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 delete_box(self, box_id: str):
|
||||
# delete 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")
|
||||
|
||||
# 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):
|
||||
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
|
||||
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)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating box: {str(e)}")
|
||||
raise e
|
75
services/file.py
Normal file
75
services/file.py
Normal 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
28
services/inventory.py
Normal 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
100
services/old_box.py
Normal 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
136
services/product.py
Normal 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
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user