GIGA FIXED EVERYTHING OMG
This commit is contained in:
136
services/file.py
136
services/file.py
@@ -1,75 +1,127 @@
|
||||
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
|
||||
from schemas.file import CreateFileRequest
|
||||
import os
|
||||
from uuid import uuid4 as uuid
|
||||
import logging
|
||||
import csv
|
||||
from io import StringIO
|
||||
from typing import Optional, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Name,Set code,Set name,Collector number,Foil,Rarity,Quantity,ManaBox ID,Scryfall ID,Purchase price,Misprint,Altered,Condition,Language,Purchase price currency
|
||||
MANABOX_REQUIRED_FILE_HEADERS = ['Name', 'Set code', 'Set name', 'Collector number', 'Foil', 'Rarity', 'Quantity', 'ManaBox ID', 'Scryfall ID', 'Purchase price', 'Misprint', 'Altered', 'Condition', 'Language', 'Purchase price currency']
|
||||
MANABOX_ALLOWED_FILE_EXTENSIONS = ['.csv']
|
||||
MANABOX_ALLOWED_FILE_TYPES = ['scan_export']
|
||||
MANABOX_CONFIG = {
|
||||
"required_headers": MANABOX_REQUIRED_FILE_HEADERS,
|
||||
"allowed_extensions": MANABOX_ALLOWED_FILE_EXTENSIONS,
|
||||
"allowed_types": MANABOX_ALLOWED_FILE_TYPES
|
||||
}
|
||||
SOURCES = {
|
||||
"manabox": MANABOX_CONFIG
|
||||
}
|
||||
TEMP_DIR = os.getcwd() + '/temp/'
|
||||
|
||||
class FileService:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
# CONFIG
|
||||
def get_config(self, source: str) -> dict:
|
||||
return SOURCES.get(source)
|
||||
|
||||
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
|
||||
# VALIDATION
|
||||
def validate_file_extension(self, filename: str, config: dict) -> bool:
|
||||
return filename.endswith(tuple(config.get("allowed_extensions")))
|
||||
|
||||
def upload_file(self, content: bytes, filename: str, metadata: FileMetadata) -> FileUploadResponse:
|
||||
# Save file to database
|
||||
def validate_file_type(self, metadata: CreateFileRequest, config: dict) -> bool:
|
||||
return metadata.type in config.get("allowed_types")
|
||||
|
||||
def validate_csv(self, content: bytes, required_headers: Optional[List[str]] = None) -> bool:
|
||||
try:
|
||||
# Try to decode and parse as CSV
|
||||
csv_text = content.decode('utf-8')
|
||||
csv_file = StringIO(csv_text)
|
||||
csv_reader = csv.reader(csv_file)
|
||||
|
||||
# Check headers if specified
|
||||
headers = next(csv_reader, None)
|
||||
if required_headers and not all(header in headers for header in required_headers):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except (UnicodeDecodeError, csv.Error):
|
||||
return False
|
||||
|
||||
def validate_file_content(self, content: bytes, metadata: CreateFileRequest, config: dict) -> bool:
|
||||
extension = metadata.filename.split('.')[-1]
|
||||
if extension == 'csv':
|
||||
return self.validate_csv(content, config.get("required_headers"))
|
||||
return False
|
||||
|
||||
def validate_file(self, content: bytes, metadata: CreateFileRequest) -> bool:
|
||||
# 1. Get config
|
||||
config = self.get_config(metadata.source)
|
||||
# 2. Validate file extension
|
||||
if not self.validate_file_extension(metadata.filename, config):
|
||||
raise Exception("Invalid file extension")
|
||||
# 2. validate file type
|
||||
if not self.validate_file_type(metadata, config):
|
||||
raise Exception("Invalid file type")
|
||||
# 3. Validate file content
|
||||
if not self.validate_file_content(content, metadata, config):
|
||||
raise Exception("Invalid file content")
|
||||
return True
|
||||
|
||||
# CRUD
|
||||
# CREATE
|
||||
def create_file(self, content: bytes, metadata: CreateFileRequest) -> File:
|
||||
with db_transaction(self.db):
|
||||
file = File(
|
||||
id = str(uuid()),
|
||||
filename = filename,
|
||||
filepath = os.getcwd() + '/temp/' + filename, # TODO: config variable
|
||||
filename = metadata.filename,
|
||||
filepath = TEMP_DIR + metadata.filename, # TODO config variable
|
||||
type = metadata.type,
|
||||
source = metadata.source,
|
||||
filesize_kb = round(len(content) / 1024,2),
|
||||
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
|
||||
return file
|
||||
|
||||
def get_file(self, file_id: str) -> File:
|
||||
return self.db.query(File).filter(File.id == file_id).first()
|
||||
# GET
|
||||
def get_file(self, file_id: str) -> List[File]:
|
||||
file = self.db.query(File).filter(File.id == file_id).first()
|
||||
if not file:
|
||||
raise Exception(f"File with id {file_id} not found")
|
||||
return file
|
||||
|
||||
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_files(self, status: Optional[str] = None) -> List[File]:
|
||||
if status:
|
||||
return self.db.query(File).filter(File.status == status).all()
|
||||
return self.db.query(File).all()
|
||||
|
||||
def get_staged_products(self, file_id: str) -> list[StagedFileProduct]:
|
||||
# DELETE
|
||||
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:
|
||||
def delete_file(self, file_id: str) -> List[File]:
|
||||
file = self.get_file(file_id)
|
||||
if not file:
|
||||
raise Exception(f"File with id {file_id} not found")
|
||||
|
||||
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")
|
||||
|
||||
with db_transaction(self.db):
|
||||
file.status = 'deleted'
|
||||
for staged_product in staged_products:
|
||||
self.db.delete(staged_product)
|
||||
|
||||
return file
|
||||
|
0
services/order.py
Normal file
0
services/order.py
Normal file
@@ -1,9 +1,10 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from db.utils import db_transaction
|
||||
from db.models import Product, File, CardManabox, Card, StagedFileProduct
|
||||
from db.models import Product, File, CardManabox, Card, StagedFileProduct, CardTCGPlayer
|
||||
from io import StringIO
|
||||
import pandas as pd
|
||||
from services.file import FileService
|
||||
from services.tcgplayer import TCGPlayerService
|
||||
from uuid import uuid4 as uuid
|
||||
import logging
|
||||
|
||||
@@ -25,9 +26,10 @@ class ManaboxRow:
|
||||
self.quantity = row['quantity']
|
||||
|
||||
class ProductService:
|
||||
def __init__(self, db: Session, file_service: FileService):
|
||||
def __init__(self, db: Session, file_service: FileService, tcgplayer_service: TCGPlayerService):
|
||||
self.db = db
|
||||
self.file_service = file_service
|
||||
self.tcgplayer_service = tcgplayer_service
|
||||
|
||||
def _format_manabox_df(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
# format columns
|
||||
@@ -81,16 +83,27 @@ class ProductService:
|
||||
)
|
||||
return product
|
||||
|
||||
def get_tcgplayer_card(self, card_manabox: CardManabox) -> CardTCGPlayer:
|
||||
# check if tcgplayer_id exists for product_id in CardTCGPlayer
|
||||
tcgplayer_card = self.db.query(CardTCGPlayer).filter(CardTCGPlayer.product_id == card_manabox.product_id).first()
|
||||
if tcgplayer_card:
|
||||
return tcgplayer_card
|
||||
# if not, get tcgplayer_id from tcgplayer_service
|
||||
tcgplayer_card = self.tcgplayer_service.get_tcgplayer_card(card_manabox)
|
||||
return tcgplayer_card
|
||||
|
||||
def create_card(self, card_manabox: CardManabox) -> Card:
|
||||
tcgplayer_card = self.get_tcgplayer_card(card_manabox)
|
||||
card = Card(
|
||||
product_id = card_manabox.product_id,
|
||||
product_id = tcgplayer_card.product_id if tcgplayer_card else 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
|
||||
manabox_id = card_manabox.manabox_id,
|
||||
tcgplayer_id = tcgplayer_card.tcgplayer_id if tcgplayer_card else None
|
||||
)
|
||||
return card
|
||||
|
||||
@@ -114,6 +127,8 @@ class ProductService:
|
||||
card_manabox = self.create_card_manabox(manabox_row)
|
||||
product = self.create_product(card_manabox)
|
||||
card = self.create_card(card_manabox)
|
||||
card_manabox.product_id = card.product_id
|
||||
product.id = card.product_id
|
||||
self.db.add(card_manabox)
|
||||
self.db.add(product)
|
||||
self.db.add(card)
|
||||
@@ -129,7 +144,7 @@ class ProductService:
|
||||
staged_product = self.create_staged_product(file, card_manabox, row)
|
||||
# update file status
|
||||
with db_transaction(self.db):
|
||||
file.status = 'prepared'
|
||||
file.status = 'completed'
|
||||
except Exception as e:
|
||||
with db_transaction(self.db):
|
||||
file.status = 'error'
|
||||
|
40
services/task.py
Normal file
40
services/task.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
import logging
|
||||
from typing import Dict, Callable
|
||||
from sqlalchemy.orm import Session
|
||||
from services.product import ProductService
|
||||
from db.models import File
|
||||
|
||||
|
||||
class TaskService:
|
||||
def __init__(self, db: Session, product_service: ProductService):
|
||||
self.scheduler = BackgroundScheduler()
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.tasks: Dict[str, Callable] = {}
|
||||
self.db = db
|
||||
self.product_service = product_service
|
||||
|
||||
async def start(self):
|
||||
self.scheduler.start()
|
||||
self.logger.info("Task scheduler started.")
|
||||
self.register_scheduled_tasks()
|
||||
|
||||
def register_scheduled_tasks(self):
|
||||
self.scheduler.add_job(
|
||||
self.daily_report,
|
||||
'cron',
|
||||
hour=0,
|
||||
minute=0,
|
||||
id='daily_report'
|
||||
)
|
||||
|
||||
# Tasks that should be scheduled
|
||||
async def daily_report(self):
|
||||
self.logger.info("Generating daily report")
|
||||
# Daily report logic
|
||||
|
||||
|
||||
async def process_manabox_file(self, file: File):
|
||||
self.logger.info("Processing ManaBox file")
|
||||
self.product_service.bg_process_manabox_file(file.id)
|
||||
self.logger.info("Finished processing ManaBox file")
|
@@ -1,4 +1,4 @@
|
||||
from db.models import ManaboxExportData, Box, TCGPlayerGroups, TCGPlayerInventory, TCGPlayerExportHistory, TCGPlayerPricingHistory, TCGPlayerProduct, ManaboxTCGPlayerMapping
|
||||
from db.models import ManaboxExportData, Box, TCGPlayerGroups, TCGPlayerInventory, TCGPlayerExportHistory, TCGPlayerPricingHistory, TCGPlayerProduct, ManaboxTCGPlayerMapping, CardManabox, CardTCGPlayer
|
||||
import requests
|
||||
from sqlalchemy.orm import Session
|
||||
from db.utils import db_transaction
|
||||
@@ -39,8 +39,7 @@ class TCGPlayerConfig:
|
||||
max_retries: int = 1
|
||||
|
||||
class TCGPlayerService:
|
||||
def __init__(self, db: Session,
|
||||
pricing_service: PricingService,
|
||||
def __init__(self, db: Session,
|
||||
config: TCGPlayerConfig=TCGPlayerConfig(),
|
||||
browser_type: Browser=Browser.BRAVE):
|
||||
self.db = db
|
||||
@@ -48,7 +47,6 @@ class TCGPlayerService:
|
||||
self.browser_type = browser_type
|
||||
self.cookies = None
|
||||
self.previous_request_time = None
|
||||
self.pricing_service = pricing_service
|
||||
|
||||
def _insert_groups(self, groups):
|
||||
for group in groups:
|
||||
@@ -282,16 +280,20 @@ class TCGPlayerService:
|
||||
|
||||
return {"message": "Inventory updated successfully", "export_id": export_id}
|
||||
|
||||
def _get_export_csv(self, set_name_ids: List[str]) -> bytes:
|
||||
def _get_export_csv(self, set_name_ids: List[str], convert=True) -> bytes:
|
||||
"""
|
||||
Download export CSV and save to specified path
|
||||
Returns True if successful, False otherwise
|
||||
"""
|
||||
logger.info(f"Downloading pricing export from tcgplayer with ids {set_name_ids}")
|
||||
payload = self._set_pricing_export_payload(set_name_ids)
|
||||
export_csv_download_url = f"{self.config.tcgplayer_base_url}{self.config.pricing_export_path}"
|
||||
response = self._send_request(export_csv_download_url, method='POST', data=payload)
|
||||
csv = self._process_content(response.content)
|
||||
return csv
|
||||
if convert:
|
||||
csv = self._process_content(response.content)
|
||||
return csv
|
||||
else:
|
||||
return response.content
|
||||
|
||||
def _update_tcgplayer_products(self):
|
||||
pass
|
||||
@@ -414,7 +416,8 @@ class TCGPlayerService:
|
||||
export_id = self.update_inventory("live")['export_id']
|
||||
self.tcg_set_tcg_inventory_product_relationship(export_id)
|
||||
self.update_pricing_for_existing_product_groups()
|
||||
update_csv = self.pricing_service.create_live_inventory_pricing_update_csv()
|
||||
# update_csv = self.pricing_service.create_live_inventory_pricing_update_csv()
|
||||
update_csv = None
|
||||
return update_csv
|
||||
|
||||
def get_group_ids_for_box(self, box_id: str) -> List[str]:
|
||||
@@ -448,5 +451,114 @@ class TCGPlayerService:
|
||||
else:
|
||||
raise ValueError("Must provide either box_id or upload_id")
|
||||
self.update_pricing({'set_name_ids': group_ids})
|
||||
add_csv = self.pricing_service.create_add_to_tcgplayer_csv(box_id)
|
||||
return add_csv
|
||||
# add_csv = self.pricing_service.create_add_to_tcgplayer_csv(box_id)
|
||||
add_csv = None
|
||||
return add_csv
|
||||
|
||||
def load_export_csv_to_card_tcgplayer(self, export_csv: bytes, group_id: int) -> None:
|
||||
if not export_csv:
|
||||
raise ValueError("No export CSV provided")
|
||||
|
||||
# Convert bytes to string first
|
||||
text_content = export_csv.decode('utf-8')
|
||||
csv_file = StringIO(text_content)
|
||||
try:
|
||||
reader = csv.DictReader(csv_file)
|
||||
for row in reader:
|
||||
product = CardTCGPlayer(
|
||||
product_id=str(uuid.uuid4()),
|
||||
tcgplayer_id=row['TCGplayer Id'],
|
||||
group_id=group_id,
|
||||
product_line=row['Product Line'],
|
||||
set_name=row['Set Name'],
|
||||
product_name=row['Product Name'],
|
||||
title=row['Title'],
|
||||
number=row['Number'],
|
||||
rarity=row['Rarity'],
|
||||
condition=row['Condition']
|
||||
)
|
||||
with db_transaction(self.db):
|
||||
self.db.add(product)
|
||||
finally:
|
||||
csv_file.close()
|
||||
|
||||
def match_card_tcgplayer_to_manabox(self, card: CardManabox, group_id: int) -> CardTCGPlayer:
|
||||
# Expanded rarity mapping
|
||||
mb_to_tcg_rarity_mapping = {
|
||||
"common": "C",
|
||||
"uncommon": "U",
|
||||
"rare": "R",
|
||||
"mythic": "M",
|
||||
"special": "S"
|
||||
}
|
||||
|
||||
# Mapping from Manabox condition+foil to TCGPlayer condition
|
||||
mb_to_tcg_condition_mapping = {
|
||||
("near_mint", "foil"): "Near Mint Foil",
|
||||
("near_mint", "normal"): "Near Mint",
|
||||
("near_mint", "etched"): "Near Mint Foil"
|
||||
}
|
||||
|
||||
# Get TCGPlayer condition from Manabox condition+foil combination
|
||||
tcg_condition = mb_to_tcg_condition_mapping.get((card.condition, card.foil))
|
||||
if tcg_condition is None:
|
||||
logger.error(f"Unsupported condition/foil combination: {card.condition}, {card.foil}")
|
||||
logger.error(f"Card details: name={card.name}, set_name={card.set_name}, collector_number={card.collector_number}")
|
||||
return None
|
||||
|
||||
# Get TCGPlayer rarity from Manabox rarity
|
||||
tcg_rarity = mb_to_tcg_rarity_mapping.get(card.rarity)
|
||||
if tcg_rarity is None:
|
||||
logger.error(f"Unsupported rarity: {card.rarity}")
|
||||
logger.error(f"Card details: name={card.name}, set_name={card.set_name}, collector_number={card.collector_number}")
|
||||
return None
|
||||
|
||||
# First query for matching products without rarity filter
|
||||
base_query = self.db.query(CardTCGPlayer).filter(
|
||||
CardTCGPlayer.number == card.collector_number,
|
||||
CardTCGPlayer.condition == tcg_condition,
|
||||
CardTCGPlayer.group_id == group_id
|
||||
)
|
||||
|
||||
# Get all potential matches
|
||||
products = base_query.all()
|
||||
|
||||
# If no products found, return None
|
||||
if not products:
|
||||
logger.error(f"No matching TCGPlayer product found for card {card.name} ({card.set_code} {card.collector_number})")
|
||||
return None
|
||||
|
||||
# Look for an exact match including rarity, unless the TCGPlayer product is a land
|
||||
for product in products:
|
||||
if product.rarity == "L" or product.rarity == tcg_rarity:
|
||||
return product
|
||||
|
||||
# If we got here, we found products but none matched our rarity criteria
|
||||
logger.error(f"No matching TCGPlayer product with correct rarity found for card {card.name} ({card.set_name} {card.collector_number})")
|
||||
return None
|
||||
|
||||
def get_tcgplayer_card(self, card: CardManabox) -> CardTCGPlayer:
|
||||
# find tcgplayer group id for set code
|
||||
group_id = self.db.query(TCGPlayerGroups.group_id).filter(
|
||||
TCGPlayerGroups.abbreviation == card.set_code
|
||||
).first()
|
||||
if not group_id:
|
||||
logger.error(f"Group ID not found for set code {card.set_code}")
|
||||
logger.error(f"Card details: name={card.name}, set_name={card.set_name}, collector_number={card.collector_number}")
|
||||
return None
|
||||
group_id = group_id[0]
|
||||
# check for group_id in CardTCGPlayer
|
||||
group_id_exists = self.db.query(CardTCGPlayer).filter(
|
||||
CardTCGPlayer.group_id == group_id).first()
|
||||
if not group_id_exists:
|
||||
export_csv = self._get_export_csv([str(group_id)], convert=False) # TODO should be file service
|
||||
self.load_export_csv_to_card_tcgplayer(export_csv, group_id)
|
||||
# match card to tcgplayer product
|
||||
matching_product = self.match_card_tcgplayer_to_manabox(card, group_id)
|
||||
if not matching_product:
|
||||
return None
|
||||
return matching_product
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user