This commit is contained in:
2025-08-01 10:33:50 -04:00
parent 82fd1cb2da
commit 9c13118a05
24 changed files with 1160 additions and 38 deletions

View File

@@ -313,6 +313,9 @@ class DataInitializationService(BaseService):
else:
# Get CSV data from API
csv_data = await tcgcsv_service.get_products_and_prices(game_id, group.group_id)
if not csv_data:
logger.warning(f"No products data found for group {group.group_id}")
continue
# Save the CSV file
await self.file_service.save_file(
@@ -428,6 +431,9 @@ class DataInitializationService(BaseService):
logger.info(f"Downloading and processing archived prices for {date}")
# Download and extract the archive
archive_data = await tcgcsv_service.get_archived_prices_for_date(date)
if not archive_data:
logger.warning(f"No archive data found for {date}")
continue
# Save the archive file
file_record = await self.file_service.save_file(

View File

@@ -67,7 +67,9 @@ class BaseExternalService:
logger.error(f"Failed to parse JSON response: {e}")
return raw_response
return raw_response
except aiohttp.ClientResponseError as e:
logger.error(f"Request failed: {e}")
raise
except aiohttp.ClientError as e:
logger.error(f"Request failed: {e}")
raise

View File

@@ -3,6 +3,9 @@ from datetime import datetime, timedelta
import csv
import io
from app.services.external_api.base_external_service import BaseExternalService
import aiohttp
import logging
logger = logging.getLogger(__name__)
class TCGCSVService(BaseExternalService):
def __init__(self):
@@ -16,7 +19,14 @@ class TCGCSVService(BaseExternalService):
async def get_products_and_prices(self, game_id: str, group_id: int) -> str:
"""Fetch products and prices for a specific group from TCGCSV API"""
endpoint = f"tcgplayer/{game_id}/{group_id}/ProductsAndPrices.csv"
return await self._make_request("GET", endpoint, headers={"Accept": "text/csv"})
try:
return await self._make_request("GET", endpoint, headers={"Accept": "text/csv"})
except aiohttp.ClientResponseError as e:
if e.status == 403:
logger.error(f"Request failed: {e}")
return None
else:
raise
async def get_categories(self) -> Dict[str, Any]:
"""Fetch all categories from TCGCSV API"""
@@ -26,7 +36,14 @@ class TCGCSVService(BaseExternalService):
async def get_archived_prices_for_date(self, date_str: str) -> bytes:
"""Fetch archived prices from TCGCSV API"""
endpoint = f"archive/tcgplayer/prices-{date_str}.ppmd.7z"
return await self._make_request("GET", endpoint, binary=True)
try:
return await self._make_request("GET", endpoint, binary=True)
except aiohttp.ClientResponseError as e:
if e.status == 403:
logger.error(f"Request failed: {e}")
return None
else:
raise
async def get_tcgcsv_date_range(self, start_date: datetime, end_date: datetime) -> List[datetime]:
"""Get a date range for a given start and end date"""

View File

@@ -10,6 +10,8 @@ class BaseTCGPlayerService(BaseExternalService):
STORE_BASE_URL = "https://store.tcgplayer.com"
LOGIN_ENDPOINT = "/oauth/login"
PRICING_ENDPOINT = "/Admin/Pricing"
MP_SEARCH_URL = "https://mp-search-api.tcgplayer.com/v1"
def __init__(self):
super().__init__(base_url=self.STORE_BASE_URL)

View File

@@ -407,8 +407,8 @@ class OrderManagementService(BaseTCGPlayerService):
('extended_price', 'extendedPrice'),
('quantity', 'quantity'),
('url', 'url'),
('tcgplayer_product_id', 'productId'),
('tcgplayer_sku_id', 'skuId')
('product_id', 'productId'),
('sku_id', 'skuId')
]
for db_field, api_field in product_fields_to_compare:

View File

@@ -0,0 +1,54 @@
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
from sqlalchemy.orm import Session
from dataclasses import dataclass
@dataclass
class TCGPlayerMarketDataInput:
product_id: str
mpfev: str
condition: str
language: str
printing: str
quantity: int
class TCGPlayerMarketDataService(BaseTCGPlayerService):
def __init__(self):
super().__init__()
async def get_active_listings(self, db: Session, input: TCGPlayerMarketDataInput):
listings_endpoint = f"{self.MP_SEARCH_URL}/product/{input.product_id}/listings?mpfev={input.mpfev}"
"""
curl 'https://mp-search-api.tcgplayer.com/v1/product/631066/listings?mpfev=3816' \
-H 'accept: application/json, text/plain, */*' \
-H 'accept-language: en-US,en;q=0.5' \
-H 'content-type: application/json' \
-b 'tracking-preferences={%22version%22:1%2C%22destinations%22:{%22Actions%20Amplitude%22:false%2C%22AdWords%22:false%2C%22Google%20AdWords%20New%22:false%2C%22Google%20Enhanced%20Conversions%22:false%2C%22Google%20Tag%20Manager%22:false%2C%22Impact%20Partnership%20Cloud%22:false%2C%22Optimizely%22:false}%2C%22custom%22:{%22advertising%22:false%2C%22functional%22:false%2C%22marketingAndAnalytics%22:false}}; tcg-uuid=4321c3a3-bdc0-4a3f-952f-ada250ea91ab; product-display-settings=sort=price+shipping&size=10; setting=CD=US&M=1; brwsr=f9b5ab04-479f-11f0-9996-95b462f705de; TCG_VisitorKey=1cfd1431-fc5d-461b-9fb3-61de387f3342; SellerProximity=ZipCode=&MaxSellerDistance=1000&IsActive=false; OAuthLoginSessionId=766a573b-d2ba-4285-8eae-c79b5c8a877c; TCGAuthTicket_Production=3C334E06A27B20FDD326A6C20C7FFDEFECD7EDB73BBE7E2A072607D3417CFDC3B1A12EDA8F5B4F393380FA2CA8FCF596476F2BEC3B54FDE788D57A05745D8820DF0897F3B673BACD6487BDA6CC0780896CB382DCFAB9AFC90B747ED5561CE5B7B8E122D0815203F93DE6EDB73894CE9CD20D6090; BuyerRevalidationKey=; ASP.NET_SessionId=s04smsk3opzinl2tl31x042r; __RequestVerificationToken_L2FkbWlu0=TnVB3O7LFL0SbCOd2ULkhadaytHVM8uXJqi8b-27w6WdPQ3QU9P76z92HmVS-i4K0SjbPDbvGe8grkme7l4m6fgetX01; LastSeller=e576ed4c; StoreSaveForLater_PRODUCTION=SFLK=4db1ce3215c84eaca7439f889cd70b79&Ignore=false; SearchCriteria=M=1&WantVerifiedSellers=False&WantDirect=False&WantSellersInCart=False&WantWPNSellers=False; SearchSortSettings=M=1&ProductSortOption=Sales&ProductSortDesc=False&PriceSortOption=Shipping&ProductResultDisplay=grid; tcg-segment-session=1749821401966%257C1749822225413' \
-H 'origin: https://www.tcgplayer.com' \
-H 'priority: u=1, i' \
-H 'referer: https://www.tcgplayer.com/' \
-H 'sec-ch-ua: "Brave";v="137", "Chromium";v="137", "Not/A)Brand";v="24"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "Linux"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-site' \
-H 'sec-gpc: 1' \
-H 'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36' \
--data-raw '{"filters":{"term":{"sellerStatus":"Live","channelId":0,"language":["English"],"condition":["Near Mint"],"printing":["Foil"]},"range":{"quantity":{"gte":2}},"exclude":{"channelExclusion":0}},"from":0,"size":10,"sort":{"field":"price+shipping","order":"asc"},"context":{"shippingCountry":"US","cart":{}},"aggregations":["listingType"]}'
"""
headers = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'en-US,en;q=0.5',
'content-type': 'application/json',
'origin': 'https://www.tcgplayer.com',
'priority': 'u=1, i',
'referer': 'https://www.tcgplayer.com/',
'sec-ch-ua': '"Brave";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': 'Linux',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'sec-gpc': '1',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
}

View File

@@ -0,0 +1,438 @@
from app.services.base_service import BaseService
from app.models.inventory_management import InventoryLabel, InventoryLabelMetadata
from app.schemas.inventory_label import InventoryLabelCreate, InventoryLabelUpdate, InventoryLabelDelete, InventoryLabelGet, InventoryLabelMetadataCreate
from app.db.database import transaction as db_transaction
import uuid as uuid_lib
import re
from sqlalchemy.orm import Session
import qrcode
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
from typing import Optional
class InventoryLabelService(BaseService):
def __init__(self):
super().__init__(None)
def _convert_uuid_to_qr_code(self, uuid_string: str) -> bytes:
"""
Convert a UUID string to a QR code image as bytes.
Args:
uuid_string: The UUID string to encode
Returns:
bytes: The QR code image as bytes
"""
# Create QR code instance
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_M, # Medium error correction for better reliability
box_size=8, # Smaller box size for better fit on labels
border=2, # Smaller border to maximize QR code size
)
# Add the UUID data to the QR code
qr.add_data(uuid_string)
qr.make(fit=True)
# Create the QR code image
img = qr.make_image(fill_color="black", back_color="white")
# Convert to bytes
img_buffer = BytesIO()
img.save(img_buffer, format='PNG')
img_buffer.seek(0)
return img_buffer.getvalue()
def _create_composite_label_image(self, qr_code_image: Image.Image, text: str, label_width: int = 991, label_height: int = 306) -> Image.Image:
"""
Create a composite image with QR code left-aligned and text right-aligned.
Args:
qr_code_image: The QR code image to place on the left
text: The text to place on the right
label_width: Width of the label in pixels
label_height: Height of the label in pixels
Returns:
Image.Image: The composite label image
"""
# Create a new white canvas
label_canvas = Image.new('RGB', (label_width, label_height), 'white')
# Calculate QR code size (square, fit within label height with margin)
qr_margin = 20
max_qr_size = label_height - (2 * qr_margin)
qr_size = min(max_qr_size, label_width // 2) # QR takes up to half the width
# Resize QR code to fit
resized_qr = qr_code_image.resize((qr_size, qr_size), Image.Resampling.LANCZOS)
# Position QR code on the left with margin
qr_x = qr_margin
qr_y = (label_height - qr_size) // 2 # Center vertically
# Paste QR code onto canvas
label_canvas.paste(resized_qr, (qr_x, qr_y))
# Add text on the right side
draw = ImageDraw.Draw(label_canvas)
# Try to use a default font, fall back to basic font if not available
font_size = 24
font = None
# Try multiple font paths
font_paths = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/System/Library/Fonts/Arial.ttf",
"/usr/share/fonts/TTF/Arial.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf"
]
for font_path in font_paths:
try:
font = ImageFont.truetype(font_path, font_size)
break
except (OSError, IOError):
continue
# Fall back to default font if no system font found
if font is None:
font = ImageFont.load_default()
# Calculate text position (right-aligned with margin)
text_margin = 20
text_x = label_width - text_margin
text_y = label_height // 2 # Center vertically
# Get text bounding box to position it properly
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
# Adjust position to right-align the text
text_x = text_x - text_width
text_y = text_y - (text_height // 2)
# Draw the text
draw.text((text_x, text_y), text, fill='black', font=font)
return label_canvas
def _create_qr_code_with_text(self, uuid_string: str, text: str) -> bytes:
"""
Create a QR code image with text and return it as bytes.
Args:
uuid_string: The UUID string to encode in QR code
text: The text to display on the label
Returns:
bytes: The composite image as bytes
"""
# Create QR code instance
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=8,
border=2,
)
# Add the UUID data to the QR code
qr.add_data(uuid_string)
qr.make(fit=True)
# Create the QR code image
qr_image = qr.make_image(fill_color="black", back_color="white")
# Create composite image with text
composite_image = self._create_composite_label_image(qr_image, text)
# Convert to bytes
img_buffer = BytesIO()
composite_image.save(img_buffer, format='PNG')
img_buffer.seek(0)
return img_buffer.getvalue()
def create_qr_code_with_text_direct(self, uuid_string: str, text: str) -> bytes:
"""
Create a QR code image with text and return it as bytes directly.
This method doesn't involve database operations and is useful for testing.
Args:
uuid_string: The UUID string to encode in QR code
text: The text to display on the label
Returns:
bytes: The composite image as bytes
"""
return self._create_qr_code_with_text(uuid_string, text)
# create
async def create_inventory_label(self, db: Session, inventory_label: InventoryLabelCreate, print: bool = True) -> InventoryLabel:
file_service = self.get_service('file')
# check if we have a upc
if inventory_label.upc:
# validate the upc
if not self._is_valid_upc(inventory_label.upc):
raise ValueError("Invalid UPC")
# check if we have metadata
if inventory_label.metadata:
# validate the metadata
for metadata in inventory_label.metadata:
if not metadata.key or not metadata.value:
raise ValueError("Invalid metadata")
# generate a uuid
label_uuid = str(uuid_lib.uuid4())
with db_transaction(db):
# create the inventory label
inventory_label_model = InventoryLabel(uuid=label_uuid, upc=inventory_label.upc)
db.add(inventory_label_model)
db.flush()
# add the metadata
if inventory_label.metadata:
for metadata in inventory_label.metadata:
inventory_label_metadata_model = InventoryLabelMetadata(inventory_label_id=inventory_label_model.id, metadata_key=metadata.key, metadata_value=metadata.value)
db.add(inventory_label_metadata_model)
if print:
# Create image with QR code and optional text
if inventory_label.metadata and len(inventory_label.metadata) > 0:
if inventory_label.upc:
# add upc to metadata
inventory_label.metadata.append(InventoryLabelMetadataCreate(key="upc", value=inventory_label.upc))
# concat metadata key values separated by newlines and :
text = "\n".join([f"{metadata.key}: {metadata.value}" for metadata in inventory_label.metadata])
# Use composite image with QR code and text
image_data = self._create_qr_code_with_text(label_uuid, text)
else:
# Use original QR code only
image_data = self._convert_uuid_to_qr_code(label_uuid)
# save file
filename = f"{label_uuid}.png"
file_record = await file_service.save_file(
db=db,
file_data=image_data,
filename=filename,
subdir="inventory_labels",
file_type="inventory_label",
content_type="image/png",
metadata={"uuid": label_uuid}
)
print_service = self.get_service('label_printer')
await print_service.print_file(file_record.path, label_size="dk1201", label_type="inventory_label", copies=1)
return inventory_label_model
# get
def classify_input_data(self, input_data: str) -> str:
"""
Classify input data as UPC, UUID, or other string with high accuracy.
Args:
input_data: The string to classify
Returns:
str: "upc", "uuid", or "other"
"""
if not input_data or not isinstance(input_data, str):
return "other"
# Remove any whitespace
input_data = input_data.strip()
# Check for UUID first (more specific pattern)
if self._is_valid_uuid(input_data):
return "uuid"
# Check for UPC code
if self._is_valid_upc(input_data):
return "upc"
return "other"
def _is_valid_uuid(self, uuid_string: str) -> bool:
"""
Validate if string is a proper UUID.
Args:
uuid_string: String to validate
Returns:
bool: True if valid UUID, False otherwise
"""
# UUID regex pattern for all versions
uuid_pattern = re.compile(
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
re.IGNORECASE
)
if not uuid_pattern.match(uuid_string):
return False
try:
# Validate UUID structure and version
uuid_obj = uuid_lib.UUID(uuid_string)
# Accept all UUID versions (1, 3, 4, 5)
return uuid_obj.version in [1, 3, 4, 5]
except ValueError:
return False
def _is_valid_upc(self, upc_string: str) -> bool:
"""
Validate if string is a proper UPC code.
Args:
upc_string: String to validate
Returns:
bool: True if valid UPC, False otherwise
"""
# Remove any non-digit characters
digits_only = re.sub(r'[^0-9]', '', upc_string)
# UPC-A must be exactly 12 digits
if len(digits_only) == 12:
return self._validate_upc_a_checksum(digits_only)
# UPC-E must be exactly 8 digits
if len(digits_only) == 8:
return self._validate_upc_e_checksum(digits_only)
# EAN-13 must be exactly 13 digits
if len(digits_only) == 13:
return self._validate_ean_13_checksum(digits_only)
return False
def _validate_upc_a_checksum(self, upc: str) -> bool:
"""
Validate UPC-A checksum.
Args:
upc: 12-digit UPC string
Returns:
bool: True if checksum is valid
"""
if len(upc) != 12 or not upc.isdigit():
return False
# Calculate checksum
total = 0
for i in range(11):
digit = int(upc[i])
if i % 2 == 0: # Odd positions (0-indexed)
total += digit * 3
else: # Even positions
total += digit
checksum = (10 - (total % 10)) % 10
return checksum == int(upc[11])
def _validate_upc_e_checksum(self, upc: str) -> bool:
"""
Validate UPC-E checksum.
Args:
upc: 8-digit UPC-E string
Returns:
bool: True if checksum is valid
"""
if len(upc) != 8 or not upc.isdigit():
return False
# Calculate checksum
total = 0
for i in range(7):
digit = int(upc[i])
if i % 2 == 0: # Odd positions (0-indexed)
total += digit * 3
else: # Even positions
total += digit
checksum = (10 - (total % 10)) % 10
return checksum == int(upc[7])
def _validate_ean_13_checksum(self, ean: str) -> bool:
"""
Validate EAN-13 checksum.
Args:
ean: 13-digit EAN string
Returns:
bool: True if checksum is valid
"""
if len(ean) != 13 or not ean.isdigit():
return False
# Calculate checksum
total = 0
for i in range(12):
digit = int(ean[i])
if i % 2 == 0: # Even positions (0-indexed)
total += digit
else: # Odd positions
total += digit * 3
checksum = (10 - (total % 10)) % 10
return checksum == int(ean[12])
async def get_inventory_label(self, db: Session, inventory_label_get: InventoryLabelGet) -> InventoryLabel:
"""
Get an inventory label by classifying the input data and querying the appropriate field.
Args:
inventory_label_get: InventoryLabelGet object containing input_data
Returns:
InventoryLabel: The found inventory label or None
"""
# check if we have a uuid or upc
if inventory_label_get.uuid:
return self._get_by_uuid(db, inventory_label_get.uuid)
elif inventory_label_get.upc:
return self._get_by_upc(db, inventory_label_get.upc)
else:
# check if we have input_data
if inventory_label_get.input_data:
# classify the input data
input_type = self.classify_input_data(inventory_label_get.input_data)
if input_type == "upc":
return self._get_by_upc(db, inventory_label_get.input_data)
elif input_type == "uuid":
return self._get_by_uuid(db, inventory_label_get.input_data)
else:
raise ValueError("Invalid input data")
else:
raise ValueError("Invalid input data")
def _get_by_upc(self, db: Session, upc: str) -> InventoryLabel:
"""
Get inventory label by UPC.
Args:
upc: The UPC code to search for
Returns:
InventoryLabel: The found inventory label or None
"""
return db.query(InventoryLabel).filter(InventoryLabel.upc == upc).first()
def _get_by_uuid(self, db: Session, uuid: str) -> InventoryLabel:
"""
Get inventory label by UUID.
Args:
uuid: The UUID to search for
Returns:
InventoryLabel: The found inventory label or None
"""
return db.query(InventoryLabel).filter(InventoryLabel.uuid == uuid).first()

View File

@@ -263,7 +263,7 @@ class BoxService(BaseService[Box]):
# ensure card
if resulting_card.item_type != "card":
raise ValueError(f"Expected card, got {resulting_card.item_type}")
resulting_card_market_value = resulting_card.products.most_recent_tcgplayer_price.market_price
resulting_card_market_value = resulting_card.products.most_recent_tcgplayer_price.market_price if resulting_card.products.most_recent_tcgplayer_price.market_price is not None else resulting_card.products.most_recent_tcgplayer_price.low_price
resulting_card_cost_basis = (resulting_card_market_value / box_expected_value) * box_cost_basis
resulting_card.inventory_item.cost_basis = resulting_card_cost_basis
db.flush()

View File

@@ -142,7 +142,7 @@ class LabelPrinterService:
logger.error(f"Unexpected error in _send_print_request: {e}")
return False
async def print_file(self, file_path: Union[str, Path, FileInDB], label_size: Literal["dk1201", "dk1241"], label_type: Optional[Literal["address_label", "packing_slip", "set_label", "return_label", "pirate_ship_label"]] = None, copies: Optional[int] = None) -> bool:
async def print_file(self, file_path: Union[str, Path, FileInDB], label_size: Literal["dk1201", "dk1241"], label_type: Optional[Literal["address_label", "packing_slip", "set_label", "return_label", "pirate_ship_label", "inventory_label"]] = None, copies: Optional[int] = None) -> bool:
"""Print a PDF or PNG file to the label printer.
Args:
@@ -207,7 +207,7 @@ class LabelPrinterService:
resized_image = resized_image.resize((991, 306), Image.Resampling.LANCZOS)
# if file path contains address_label, rotate image 90 degrees
if label_type == "address_label" or label_type == "set_label" or label_type == "return_label":
if label_type == "address_label" or label_type == "set_label" or label_type == "return_label" or label_type == "inventory_label":
rotate = "90"
cut = False
else:

View File

@@ -183,7 +183,7 @@ class PricingService(BaseService):
"""
# Fetch base pricing data
cost_basis = price_data.cost_basis
market_price = price_data.market_price
market_price = price_data.market_price if price_data.market_price is not None else price_data.tcg_mid
tcg_low = price_data.tcg_low
tcg_mid = price_data.tcg_mid
listed_price = price_data.listed_price

View File

@@ -87,28 +87,38 @@ class PullSheetService(BaseService):
Args:
row: pandas Series
"""
# if rarity is nan, return none
if pd.isna(row['Rarity']):
return '?'
# get category id from set name
group_id = db.query(TCGPlayerGroup).filter(TCGPlayerGroup.name == row['Set']).first().group_id
# format number
number = str(int(row['Number'])) if 'Number' in row and pd.notna(row['Number']) and '/' not in str(row['Number']) else str(row['Number']) if 'Number' in row and pd.notna(row['Number']) and '/' in str(row['Number']) else ''
# format number - convert float to int if it's a pure number, otherwise keep as is
number = str(int(float(row['Number']))) if 'Number' in row and pd.notna(row['Number']) and str(row['Number']).replace('.', '').isdigit() else str(row['Number']) if 'Number' in row and pd.notna(row['Number']) else ''
# get product info from category id
product_id = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.group_id == group_id).filter(TCGPlayerProduct.name == row['Product Name']).filter(TCGPlayerProduct.ext_number == number).filter(TCGPlayerProduct.ext_rarity == row['Rarity']).first().tcgplayer_product_id
# only do this block if mtg, to do fix this more betterer
# get scryfall id from product id
mtgjson_id = db.query(MTGJSONSKU).filter(MTGJSONSKU.tcgplayer_product_id == product_id).first().mtgjson_uuid
scryfall_id = db.query(MTGJSONCard).filter(MTGJSONCard.mtgjson_uuid == mtgjson_id).first().scryfall_id
# get color identity from scryfall
scryfall_service = self.get_service('scryfall')
color_identity = await scryfall_service.get_color_identity(scryfall_id)
if color_identity is None:
return '?'
# color identity is str of json array, convert to human readable string of list
color_identity = [str(color) for color in color_identity]
# if color identity is empty, return C for colorless
if not color_identity:
return 'C'
# ensure order, W > U > B > R > G
color_identity = sorted(color_identity, key=lambda x: ['W', 'U', 'B', 'R', 'G'].index(x))
color_identity = ''.join(color_identity)
mtgjson_id = db.query(MTGJSONSKU).filter(MTGJSONSKU.tcgplayer_product_id == product_id).first()
if mtgjson_id is not None:
scryfall_id = db.query(MTGJSONCard).filter(MTGJSONCard.mtgjson_uuid == mtgjson_id.mtgjson_uuid).first().scryfall_id
else:
scryfall_id = None
if scryfall_id is not None:
# get color identity from scryfall
scryfall_service = self.get_service('scryfall')
color_identity = await scryfall_service.get_color_identity(scryfall_id)
if color_identity is None:
return '?'
# color identity is str of json array, convert to human readable string of list
color_identity = [str(color) for color in color_identity]
# if color identity is empty, return C for colorless
if not color_identity:
return 'C'
# ensure order, W > U > B > R > G
color_identity = sorted(color_identity, key=lambda x: ['W', 'U', 'B', 'R', 'G'].index(x))
color_identity = ''.join(color_identity)
else:
color_identity = '?'
return color_identity
async def _update_row_color_identity(self, db: Session, row: pd.Series) -> pd.Series:
@@ -166,7 +176,7 @@ class PullSheetService(BaseService):
'quantity': str(int(row['Quantity'])), # Convert to string for template
'set': row['Set'],
'rarity': row['Rarity'],
'card_number': str(int(row['Number'])) if 'Number' in row and pd.notna(row['Number']) and '/' not in str(row['Number']) else str(row['Number']) if 'Number' in row and pd.notna(row['Number']) and '/' in str(row['Number']) else '',
'card_number': str(int(float(row['Number']))) if 'Number' in row and pd.notna(row['Number']) and str(row['Number']).replace('.', '').isdigit() else str(row['Number']) if 'Number' in row and pd.notna(row['Number']) else '',
'color_identity': row['Color Identity']
})

View File

@@ -35,7 +35,9 @@ class ServiceManager:
'box': 'app.services.inventory_service.BoxService',
'case': 'app.services.inventory_service.CaseService',
'marketplace_listing': 'app.services.inventory_service.MarketplaceListingService',
'scryfall': 'app.services.external_api.scryfall.scryfall_service.ScryfallService'
'scryfall': 'app.services.external_api.scryfall.scryfall_service.ScryfallService',
'tcgplayer_market_data': 'app.services.external_api.tcgplayer.tcgplayer_market_data_service.TCGPlayerMarketDataService',
'inventory_label': 'app.services.inventory_label_service.InventoryLabelService'
}
self._service_configs = {