so much stuff lol

This commit is contained in:
2025-04-09 23:53:05 -04:00
parent 1c00ea8569
commit df6490cab0
40 changed files with 1909 additions and 277 deletions

View File

@@ -1,5 +1,15 @@
from app.services.base_service import BaseService
from app.services.card_service import CardService
from app.services.order_service import OrderService
from app.services.file_processing_service import FileProcessingService
from app.services.inventory_service import InventoryService
from app.services.service_registry import ServiceRegistry
__all__ = ["BaseService", "CardService", "ServiceRegistry"]
__all__ = [
'BaseService',
'CardService',
'OrderService',
'FileProcessingService',
'InventoryService',
'ServiceRegistry'
]

View File

@@ -1,17 +1,126 @@
from typing import List, Optional
from typing import List, Optional, Dict
from sqlalchemy.orm import Session
from app.services.base_service import BaseService
from app.models.card import Card
from app.services.base_service import BaseService
from app.schemas.card import CardCreate, CardUpdate
class CardService(BaseService[Card]):
def __init__(self):
super().__init__(Card)
def create(self, db: Session, obj_in: Dict) -> Card:
"""
Create a new card in the database.
Args:
db: Database session
obj_in: Dictionary containing card data
Returns:
Card: The created card object
"""
return super().create(db, obj_in)
def update(self, db: Session, db_obj: Card, obj_in: Dict) -> Card:
"""
Update an existing card in the database.
Args:
db: Database session
db_obj: The card object to update
obj_in: Dictionary containing updated card data
Returns:
Card: The updated card object
"""
return super().update(db, db_obj, obj_in)
def get_by_name(self, db: Session, name: str) -> Optional[Card]:
"""
Get a card by its name.
Args:
db: Database session
name: The name of the card to find
Returns:
Optional[Card]: The card if found, None otherwise
"""
return db.query(self.model).filter(self.model.name == name).first()
def get_by_set(self, db: Session, set_name: str, skip: int = 0, limit: int = 100) -> List[Card]:
"""
Get all cards from a specific set.
Args:
db: Database session
set_name: The name of the set to filter by
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
List[Card]: List of cards from the specified set
"""
return db.query(self.model).filter(self.model.set_name == set_name).offset(skip).limit(limit).all()
def get_by_rarity(self, db: Session, rarity: str, skip: int = 0, limit: int = 100) -> List[Card]:
"""
Get all cards of a specific rarity.
Args:
db: Database session
rarity: The rarity to filter by
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
List[Card]: List of cards with the specified rarity
"""
return db.query(self.model).filter(self.model.rarity == rarity).offset(skip).limit(limit).all()
def get_by_set(self, db: Session, set_name: str, skip: int = 0, limit: int = 100) -> List[Card]:
return db.query(self.model).filter(self.model.set_name == set_name).offset(skip).limit(limit).all()
def update_quantity(self, db: Session, card_id: int, quantity_change: int) -> Card:
"""
Update the quantity of a card.
Args:
db: Database session
card_id: The ID of the card to update
quantity_change: The amount to change the quantity by (can be positive or negative)
Returns:
Card: The updated card object
Raises:
ValueError: If the card is not found or if the resulting quantity would be negative
"""
card = self.get(db, card_id)
if not card:
raise ValueError(f"Card with ID {card_id} not found")
new_quantity = card.quantity + quantity_change
if new_quantity < 0:
raise ValueError(f"Cannot reduce quantity below 0. Current quantity: {card.quantity}, attempted change: {quantity_change}")
card.quantity = new_quantity
db.add(card)
db.commit()
db.refresh(card)
return card
def search(self, db: Session, query: str, skip: int = 0, limit: int = 100) -> List[Card]:
"""
Search for cards by name or set name.
Args:
db: Database session
query: The search query
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
List[Card]: List of cards matching the search query
"""
return db.query(self.model).filter(
(self.model.name.ilike(f"%{query}%")) |
(self.model.set_name.ilike(f"%{query}%"))
).offset(skip).limit(limit).all()

View File

@@ -0,0 +1,201 @@
import os
import json
from datetime import datetime
from typing import Optional, List
from sqlalchemy.orm import Session
from app.services.external_api.tcgcsv.tcgcsv_service import TCGCSVService
from app.models.tcgplayer_group import TCGPlayerGroup
from app.models.tcgplayer_product import TCGPlayerProduct
from app.models.tcgplayer_category import TCGPlayerCategory
class DataInitializationService:
def __init__(self, cache_dir: str = "app/data/cache/tcgcsv"):
self.cache_dir = cache_dir
self.tcgcsv_service = TCGCSVService()
os.makedirs(cache_dir, exist_ok=True)
def _get_cache_path(self, filename: str) -> str:
return os.path.join(self.cache_dir, filename)
async def _cache_categories(self, categories_data: dict):
"""Cache categories data to a JSON file"""
cache_path = self._get_cache_path("categories.json")
with open(cache_path, 'w') as f:
json.dump(categories_data, f, indent=2)
async def _cache_groups(self, game_ids: List[int], groups_data: dict):
for game_id in game_ids:
cache_path = self._get_cache_path(f"groups_{game_id}.json")
with open(cache_path, 'w') as f:
json.dump(groups_data, f, default=str)
async def _cache_products(self, game_ids: List[int], group_id: int, products_data: list):
for game_id in game_ids:
cache_path = self._get_cache_path(f"products_{game_id}_{group_id}.json")
with open(cache_path, 'w') as f:
json.dump(products_data, f, default=str)
async def _load_cached_categories(self) -> Optional[dict]:
cache_path = self._get_cache_path("categories.json")
if os.path.exists(cache_path):
with open(cache_path, 'r') as f:
return json.load(f)
return None
async def _load_cached_groups(self, game_ids: List[int]) -> Optional[dict]:
# Try to load cached data for any of the game IDs
for game_id in game_ids:
cache_path = self._get_cache_path(f"groups_{game_id}.json")
if os.path.exists(cache_path):
with open(cache_path, 'r') as f:
return json.load(f)
return None
async def _load_cached_products(self, game_ids: List[int], group_id: int) -> Optional[list]:
# Try to load cached data for any of the game IDs
for game_id in game_ids:
cache_path = self._get_cache_path(f"products_{game_id}_{group_id}.json")
if os.path.exists(cache_path):
with open(cache_path, 'r') as f:
return json.load(f)
return None
async def initialize_data(self, db: Session, game_ids: List[int], use_cache: bool = True) -> None:
"""Initialize TCGPlayer data, using cache if available and requested"""
print("Initializing TCGPlayer data...")
# Handle categories
categories_data = None
if use_cache:
categories_data = await self._load_cached_categories()
if not categories_data:
print("Fetching categories from API...")
categories_data = await self.tcgcsv_service.get_categories()
if use_cache:
await self._cache_categories(categories_data)
if not categories_data.get("success"):
raise Exception(f"Failed to fetch categories: {categories_data.get('errors')}")
# Sync categories to database
categories = categories_data.get("results", [])
synced_categories = []
for category_data in categories:
existing_category = db.query(TCGPlayerCategory).filter(TCGPlayerCategory.category_id == category_data["categoryId"]).first()
if existing_category:
synced_categories.append(existing_category)
else:
new_category = TCGPlayerCategory(
category_id=category_data["categoryId"],
name=category_data["name"],
display_name=category_data.get("displayName"),
seo_category_name=category_data.get("seoCategoryName"),
category_description=category_data.get("categoryDescription"),
category_page_title=category_data.get("categoryPageTitle"),
sealed_label=category_data.get("sealedLabel"),
non_sealed_label=category_data.get("nonSealedLabel"),
condition_guide_url=category_data.get("conditionGuideUrl"),
is_scannable=category_data.get("isScannable", False),
popularity=category_data.get("popularity", 0),
is_direct=category_data.get("isDirect", False),
modified_on=datetime.fromisoformat(category_data["modifiedOn"].replace("Z", "+00:00")) if category_data.get("modifiedOn") else None
)
db.add(new_category)
synced_categories.append(new_category)
db.commit()
print(f"Synced {len(synced_categories)} categories")
# Process each game ID separately
for game_id in game_ids:
print(f"\nProcessing game ID: {game_id}")
# Handle groups for this game ID
groups_data = None
if use_cache:
groups_data = await self._load_cached_groups([game_id])
if not groups_data:
print(f"Fetching groups for game ID {game_id} from API...")
groups_data = await self.tcgcsv_service.get_groups([game_id])
if use_cache:
await self._cache_groups([game_id], groups_data)
if not groups_data.get("success"):
raise Exception(f"Failed to fetch groups for game ID {game_id}: {groups_data.get('errors')}")
# Sync groups to database
groups = groups_data.get("results", [])
synced_groups = []
for group_data in groups:
existing_group = db.query(TCGPlayerGroup).filter(TCGPlayerGroup.group_id == group_data["groupId"]).first()
if existing_group:
synced_groups.append(existing_group)
else:
new_group = TCGPlayerGroup(
group_id=group_data["groupId"],
name=group_data["name"],
abbreviation=group_data.get("abbreviation"),
is_supplemental=group_data.get("isSupplemental", False),
published_on=datetime.fromisoformat(group_data["publishedOn"].replace("Z", "+00:00")) if group_data.get("publishedOn") else None,
modified_on=datetime.fromisoformat(group_data["modifiedOn"].replace("Z", "+00:00")) if group_data.get("modifiedOn") else None,
category_id=group_data.get("categoryId")
)
db.add(new_group)
synced_groups.append(new_group)
db.commit()
print(f"Synced {len(synced_groups)} groups for game ID {game_id}")
# Handle products for each group in this game ID
for group in synced_groups:
products_data = None
if use_cache:
products_data = await self._load_cached_products([game_id], group.group_id)
if not products_data:
print(f"Fetching products for group {group.name} (game ID {game_id}) from API...")
products_data = await self.tcgcsv_service.get_products_and_prices([game_id], group.group_id)
if use_cache:
await self._cache_products([game_id], group.group_id, products_data)
# Sync products to database
synced_products = []
for product_data in products_data:
existing_product = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.product_id == int(product_data["productId"])).first()
if existing_product:
synced_products.append(existing_product)
else:
new_product = TCGPlayerProduct(
product_id=int(product_data["productId"]),
name=product_data["name"],
clean_name=product_data.get("cleanName"),
image_url=product_data.get("imageUrl"),
category_id=int(product_data["categoryId"]),
group_id=int(product_data["groupId"]),
url=product_data.get("url"),
modified_on=datetime.fromisoformat(product_data["modifiedOn"].replace("Z", "+00:00")) if product_data.get("modifiedOn") else None,
image_count=int(product_data.get("imageCount", 0)),
ext_rarity=product_data.get("extRarity"),
ext_number=product_data.get("extNumber"),
low_price=float(product_data.get("lowPrice")) if product_data.get("lowPrice") else None,
mid_price=float(product_data.get("midPrice")) if product_data.get("midPrice") else None,
high_price=float(product_data.get("highPrice")) if product_data.get("highPrice") else None,
market_price=float(product_data.get("marketPrice")) if product_data.get("marketPrice") else None,
direct_low_price=float(product_data.get("directLowPrice")) if product_data.get("directLowPrice") else None,
sub_type_name=product_data.get("subTypeName")
)
db.add(new_product)
synced_products.append(new_product)
db.commit()
print(f"Synced {len(synced_products)} products for group {group.name} (game ID {game_id})")
async def clear_cache(self) -> None:
"""Clear all cached data"""
for filename in os.listdir(self.cache_dir):
file_path = os.path.join(self.cache_dir, filename)
if os.path.isfile(file_path):
os.unlink(file_path)
print("Cache cleared")
async def close(self):
await self.tcgcsv_service.close()

View File

@@ -1,7 +1,8 @@
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Union
import aiohttp
import logging
from app.services.service_registry import ServiceRegistry
import json
logger = logging.getLogger(__name__)
@@ -24,8 +25,9 @@ class BaseExternalService:
endpoint: str,
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
data: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
data: Optional[Dict[str, Any]] = None,
content_type: str = "application/json"
) -> Union[Dict[str, Any], str]:
session = await self._get_session()
url = f"{self.base_url}{endpoint}"
@@ -36,9 +38,30 @@ class BaseExternalService:
try:
async with session.request(method, url, params=params, headers=headers, json=data) as response:
response.raise_for_status()
return await response.json()
# Get the actual content type from the response
response_content_type = response.headers.get('content-type', '').lower()
logger.info(f"Making request to {url}")
# Get the raw response text first
raw_response = await response.text()
# Only try to parse as JSON if the content type indicates JSON
if 'application/json' in response_content_type or 'text/json' in response_content_type:
try:
# First try to parse the response directly
return await response.json()
except Exception as e:
try:
# If that fails, try parsing the raw text as JSON (in case it's double-encoded)
return json.loads(raw_response)
except Exception as e:
logger.error(f"Failed to parse JSON response: {e}")
return raw_response
return raw_response
except aiohttp.ClientError as e:
logger.error(f"API request failed: {str(e)}")
logger.error(f"Request failed: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error during API request: {str(e)}")

View File

@@ -0,0 +1,205 @@
from typing import List, Dict, Any
from datetime import datetime
import csv
import io
from app.services.external_api.base_external_service import BaseExternalService
from app.models.tcgplayer_group import TCGPlayerGroup
from app.models.tcgplayer_product import TCGPlayerProduct
from app.models.tcgplayer_category import TCGPlayerCategory
from sqlalchemy.orm import Session
class TCGCSVService(BaseExternalService):
def __init__(self):
super().__init__(base_url="https://tcgcsv.com/tcgplayer/")
async def get_groups(self, game_ids: List[int]) -> Dict[str, Any]:
"""Fetch groups for specific game IDs from TCGCSV API"""
game_ids_str = ",".join(map(str, game_ids))
endpoint = f"{game_ids_str}/groups"
return await self._make_request("GET", endpoint)
async def get_products_and_prices(self, game_ids: List[int], group_id: int) -> List[Dict[str, Any]]:
"""Fetch products and prices for a specific group from TCGCSV API"""
game_ids_str = ",".join(map(str, game_ids))
endpoint = f"{game_ids_str}/{group_id}/ProductsAndPrices.csv"
response = await self._make_request("GET", endpoint, headers={"Accept": "text/csv"})
# Parse CSV response
csv_data = io.StringIO(response)
reader = csv.DictReader(csv_data)
return list(reader)
async def get_categories(self) -> Dict[str, Any]:
"""Fetch all categories from TCGCSV API"""
endpoint = "categories"
return await self._make_request("GET", endpoint)
async def sync_groups_to_db(self, db: Session, game_ids: List[int]) -> List[TCGPlayerGroup]:
"""Fetch groups from API and sync them to the database"""
response = await self.get_groups(game_ids)
if not response.get("success"):
raise Exception(f"Failed to fetch groups: {response.get('errors')}")
groups = response.get("results", [])
synced_groups = []
for group_data in groups:
# Convert string dates to datetime objects
published_on = datetime.fromisoformat(group_data["publishedOn"].replace("Z", "+00:00")) if group_data.get("publishedOn") else None
modified_on = datetime.fromisoformat(group_data["modifiedOn"].replace("Z", "+00:00")) if group_data.get("modifiedOn") else None
# Check if group already exists
existing_group = db.query(TCGPlayerGroup).filter(TCGPlayerGroup.group_id == group_data["groupId"]).first()
if existing_group:
# Update existing group
for key, value in {
"name": group_data["name"],
"abbreviation": group_data.get("abbreviation"),
"is_supplemental": group_data.get("isSupplemental", False),
"published_on": published_on,
"modified_on": modified_on,
"category_id": group_data.get("categoryId")
}.items():
setattr(existing_group, key, value)
synced_groups.append(existing_group)
else:
# Create new group
new_group = TCGPlayerGroup(
group_id=group_data["groupId"],
name=group_data["name"],
abbreviation=group_data.get("abbreviation"),
is_supplemental=group_data.get("isSupplemental", False),
published_on=published_on,
modified_on=modified_on,
category_id=group_data.get("categoryId")
)
db.add(new_group)
synced_groups.append(new_group)
db.commit()
return synced_groups
async def sync_products_to_db(self, db: Session, game_id: int, group_id: int) -> List[TCGPlayerProduct]:
"""Fetch products and prices for a group and sync them to the database"""
products_data = await self.get_products_and_prices(game_id, group_id)
synced_products = []
for product_data in products_data:
# Convert string dates to datetime objects
modified_on = datetime.fromisoformat(product_data["modifiedOn"].replace("Z", "+00:00")) if product_data.get("modifiedOn") else None
# Convert price strings to floats, handling empty strings
def parse_price(price_str):
return float(price_str) if price_str else None
# Check if product already exists
existing_product = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.product_id == int(product_data["productId"])).first()
if existing_product:
# Update existing product
for key, value in {
"name": product_data["name"],
"clean_name": product_data.get("cleanName"),
"image_url": product_data.get("imageUrl"),
"category_id": int(product_data["categoryId"]),
"group_id": int(product_data["groupId"]),
"url": product_data.get("url"),
"modified_on": modified_on,
"image_count": int(product_data.get("imageCount", 0)),
"ext_rarity": product_data.get("extRarity"),
"ext_number": product_data.get("extNumber"),
"low_price": parse_price(product_data.get("lowPrice")),
"mid_price": parse_price(product_data.get("midPrice")),
"high_price": parse_price(product_data.get("highPrice")),
"market_price": parse_price(product_data.get("marketPrice")),
"direct_low_price": parse_price(product_data.get("directLowPrice")),
"sub_type_name": product_data.get("subTypeName")
}.items():
setattr(existing_product, key, value)
synced_products.append(existing_product)
else:
# Create new product
new_product = TCGPlayerProduct(
product_id=int(product_data["productId"]),
name=product_data["name"],
clean_name=product_data.get("cleanName"),
image_url=product_data.get("imageUrl"),
category_id=int(product_data["categoryId"]),
group_id=int(product_data["groupId"]),
url=product_data.get("url"),
modified_on=modified_on,
image_count=int(product_data.get("imageCount", 0)),
ext_rarity=product_data.get("extRarity"),
ext_number=product_data.get("extNumber"),
low_price=parse_price(product_data.get("lowPrice")),
mid_price=parse_price(product_data.get("midPrice")),
high_price=parse_price(product_data.get("highPrice")),
market_price=parse_price(product_data.get("marketPrice")),
direct_low_price=parse_price(product_data.get("directLowPrice")),
sub_type_name=product_data.get("subTypeName")
)
db.add(new_product)
synced_products.append(new_product)
db.commit()
return synced_products
async def sync_categories_to_db(self, db: Session) -> List[TCGPlayerCategory]:
"""Fetch categories from API and sync them to the database"""
response = await self.get_categories()
if not response.get("success"):
raise Exception(f"Failed to fetch categories: {response.get('errors')}")
categories = response.get("results", [])
synced_categories = []
for category_data in categories:
# Convert string dates to datetime objects
modified_on = datetime.fromisoformat(category_data["modifiedOn"].replace("Z", "+00:00")) if category_data.get("modifiedOn") else None
# Check if category already exists
existing_category = db.query(TCGPlayerCategory).filter(TCGPlayerCategory.category_id == category_data["categoryId"]).first()
if existing_category:
# Update existing category
for key, value in {
"name": category_data["name"],
"display_name": category_data.get("displayName"),
"seo_category_name": category_data.get("seoCategoryName"),
"category_description": category_data.get("categoryDescription"),
"category_page_title": category_data.get("categoryPageTitle"),
"sealed_label": category_data.get("sealedLabel"),
"non_sealed_label": category_data.get("nonSealedLabel"),
"condition_guide_url": category_data.get("conditionGuideUrl"),
"is_scannable": category_data.get("isScannable", False),
"popularity": category_data.get("popularity", 0),
"is_direct": category_data.get("isDirect", False),
"modified_on": modified_on
}.items():
setattr(existing_category, key, value)
synced_categories.append(existing_category)
else:
# Create new category
new_category = TCGPlayerCategory(
category_id=category_data["categoryId"],
name=category_data["name"],
display_name=category_data.get("displayName"),
seo_category_name=category_data.get("seoCategoryName"),
category_description=category_data.get("categoryDescription"),
category_page_title=category_data.get("categoryPageTitle"),
sealed_label=category_data.get("sealedLabel"),
non_sealed_label=category_data.get("nonSealedLabel"),
condition_guide_url=category_data.get("conditionGuideUrl"),
is_scannable=category_data.get("isScannable", False),
popularity=category_data.get("popularity", 0),
is_direct=category_data.get("isDirect", False),
modified_on=modified_on
)
db.add(new_category)
synced_categories.append(new_category)
db.commit()
return synced_categories

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Union
import aiohttp
import logging
from app.services.external_api.base_external_service import BaseExternalService
@@ -7,14 +7,20 @@ from app.services.external_api.tcgplayer.tcgplayer_credentials import TCGPlayerC
logger = logging.getLogger(__name__)
class BaseTCGPlayerService(BaseExternalService):
STORE_BASE_URL = "https://store.tcgplayer.com"
LOGIN_ENDPOINT = "/oauth/login"
PRICING_ENDPOINT = "/Admin/Pricing"
def __init__(self):
super().__init__(
store_base_url="https://store.tcgplayer.com",
login_endpoint="/oauth/login",
pricing_endpoint="/Admin/Pricing",
staged_inventory_endpoint=self.pricing_endpoint + "/DownloadStagedInventoryExportCSV?type=Pricing",
live_inventory_endpoint=self.pricing_endpoint + "/DownloadMyExportCSV?type=Pricing"
)
super().__init__(base_url=self.STORE_BASE_URL)
# Set up endpoints
self.login_endpoint = self.LOGIN_ENDPOINT
self.pricing_endpoint = self.PRICING_ENDPOINT
self.staged_inventory_endpoint = f"{self.PRICING_ENDPOINT}/DownloadStagedInventoryExportCSV?type=Pricing"
self.live_inventory_endpoint = f"{self.PRICING_ENDPOINT}/DownloadMyExportCSV?type=Pricing"
self.pricing_export_endpoint = f"{self.PRICING_ENDPOINT}/downloadexportcsv"
self.credentials = TCGPlayerCredentials()
def _get_headers(self, method: str) -> Dict[str, str]:
@@ -53,10 +59,11 @@ class BaseTCGPlayerService(BaseExternalService):
params: Optional[Dict[str, Any]] = None,
headers: Optional[Dict[str, str]] = None,
data: Optional[Dict[str, Any]] = None,
auth_required: bool = True
) -> Dict[str, Any]:
auth_required: bool = True,
download_file: bool = False
) -> Union[Dict[str, Any], bytes]:
session = await self._get_session()
url = f"{self.store_base_url}{endpoint}"
url = f"{self.base_url}{endpoint}"
# Get the authentication cookie if required
if auth_required:
@@ -77,6 +84,9 @@ class BaseTCGPlayerService(BaseExternalService):
if response.status == 401:
raise RuntimeError("TCGPlayer authentication failed. Cookie may be invalid or expired.")
response.raise_for_status()
if download_file:
return await response.read()
return await response.json()
except aiohttp.ClientError as e:
logger.error(f"TCGPlayer API request failed: {str(e)}")

View File

@@ -5,14 +5,17 @@ class TCGPlayerInventoryService(BaseTCGPlayerService):
def __init__(self):
super().__init__()
async def get_inventory(self) -> List[Dict]:
"""Get inventory items"""
endpoint = "/inventory"
response = await self._make_request("GET", endpoint)
return response.get("results", [])
async def update_inventory(self, updates: List[Dict]) -> Dict:
"""Update inventory items"""
endpoint = "/inventory"
response = await self._make_request("PUT", endpoint, data=updates)
return response
async def get_tcgplayer_export(self, export_type: str):
"""
Get a TCGPlayer Staged Inventory Export, Live Inventory Export, or Pricing Export
"""
if export_type == "staged":
endpoint = self.staged_inventory_endpoint
elif export_type == "live":
endpoint = self.live_inventory_endpoint
elif export_type == "pricing":
endpoint = self.pricing_export_endpoint
else:
raise ValueError(f"Invalid export type: {export_type}, must be 'staged', 'live', or 'pricing'")
file_bytes = await self._make_request("GET", endpoint, download_file=True)
return file_bytes

View File

@@ -1,106 +0,0 @@
from typing import Dict, List, Optional
from datetime import datetime
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
class TCGPlayerOrderService(BaseTCGPlayerService):
def __init__(self):
super().__init__()
async def get_orders(
self,
status: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
limit: int = 100
) -> List[Dict]:
"""
Get a list of orders with optional filtering
Args:
status: Filter by order status (e.g., "Shipped", "Processing")
start_date: Filter orders after this date
end_date: Filter orders before this date
limit: Maximum number of orders to return
Returns:
List of orders matching the criteria
"""
endpoint = "/orders"
params = {"limit": limit}
if status:
params["status"] = status
if start_date:
params["startDate"] = start_date.isoformat()
if end_date:
params["endDate"] = end_date.isoformat()
response = await self._make_request("GET", endpoint, params=params)
return response.get("results", [])
async def get_order_details(self, order_id: str) -> Dict:
"""
Get detailed information about a specific order
Args:
order_id: TCGPlayer order ID
Returns:
Detailed order information
"""
endpoint = f"/orders/{order_id}"
response = await self._make_request("GET", endpoint)
return response
async def get_order_items(self, order_id: str) -> List[Dict]:
"""
Get items in a specific order
Args:
order_id: TCGPlayer order ID
Returns:
List of items in the order
"""
endpoint = f"/orders/{order_id}/items"
response = await self._make_request("GET", endpoint)
return response.get("results", [])
async def get_order_status(self, order_id: str) -> Dict:
"""
Get the current status of an order
Args:
order_id: TCGPlayer order ID
Returns:
Order status information
"""
endpoint = f"/orders/{order_id}/status"
response = await self._make_request("GET", endpoint)
return response
async def update_order_status(
self,
order_id: str,
status: str,
tracking_number: Optional[str] = None
) -> Dict:
"""
Update the status of an order
Args:
order_id: TCGPlayer order ID
status: New status for the order
tracking_number: Optional tracking number for shipping
Returns:
Updated order information
"""
endpoint = f"/orders/{order_id}/status"
data = {"status": status}
if tracking_number:
data["trackingNumber"] = tracking_number
response = await self._make_request("PUT", endpoint, data=data)
return response

View File

@@ -0,0 +1,146 @@
from typing import Optional, List, Dict
import csv
import io
import os
import json
from datetime import datetime
from sqlalchemy.orm import Session
from app.db.database import transaction
from app.models.inventory import Inventory
from app.models.tcgplayer_product import TCGPlayerProduct
from app.services.inventory_service import InventoryService
class FileProcessingService:
def __init__(self, cache_dir: str = "app/data/cache/tcgplayer"):
self.cache_dir = cache_dir
self.inventory_service = InventoryService()
os.makedirs(cache_dir, exist_ok=True)
def _get_cache_path(self, filename: str) -> str:
return os.path.join(self.cache_dir, filename)
async def _cache_export(self, file_bytes: bytes, export_type: str):
cache_path = self._get_cache_path(f"{export_type}_export.csv")
with open(cache_path, 'wb') as f:
f.write(file_bytes)
async def _load_cached_export(self, export_type: str) -> Optional[bytes]:
cache_path = self._get_cache_path(f"{export_type}_export.csv")
if os.path.exists(cache_path):
with open(cache_path, 'rb') as f:
return f.read()
return None
async def process_tcgplayer_export(self, db: Session, file_bytes: bytes, export_type: str = "live", use_cache: bool = False) -> dict:
"""
Process a TCGPlayer export file and load it into the inventory table.
Args:
db: Database session
file_bytes: The downloaded file content as bytes
export_type: Type of export (staged, live, pricing)
use_cache: Whether to use cached export file for development
Returns:
dict: Processing statistics
"""
stats = {
"total_rows": 0,
"processed_rows": 0,
"errors": 0,
"error_messages": []
}
try:
# For development, use cached file if available
if use_cache:
cached_bytes = await self._load_cached_export(export_type)
if cached_bytes:
file_bytes = cached_bytes
else:
await self._cache_export(file_bytes, export_type)
# Convert bytes to string and create a file-like object
file_content = file_bytes.decode('utf-8')
file_like = io.StringIO(file_content)
# Read CSV file
csv_reader = csv.DictReader(file_like)
with transaction(db):
for row in csv_reader:
stats["total_rows"] += 1
try:
# Process each row and create/update inventory item in database
inventory_data = self._map_tcgplayer_row_to_inventory(row)
tcgplayer_id = inventory_data["tcgplayer_id"]
# Check if inventory item already exists
existing_item = self.inventory_service.get_by_tcgplayer_id(db, tcgplayer_id)
# Find matching TCGPlayer product
product_id = int(tcgplayer_id) if tcgplayer_id.isdigit() else None
if product_id:
tcg_product = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.product_id == product_id).first()
if tcg_product:
# Update inventory data with product information if available
inventory_data.update({
"product_name": tcg_product.name,
"photo_url": tcg_product.image_url,
"rarity": tcg_product.ext_rarity,
"number": tcg_product.ext_number
})
if existing_item:
# Update existing item
self.inventory_service.update(db, existing_item, inventory_data)
else:
# Create new item
self.inventory_service.create(db, inventory_data)
stats["processed_rows"] += 1
except Exception as e:
stats["errors"] += 1
stats["error_messages"].append(f"Error processing row {stats['total_rows']}: {str(e)}")
return stats
except Exception as e:
raise Exception(f"Failed to process TCGPlayer export: {str(e)}")
def _map_tcgplayer_row_to_inventory(self, row: dict) -> dict:
"""
Map TCGPlayer export row to inventory model fields.
"""
def safe_float(value: str) -> float:
"""Convert string to float, returning 0.0 for empty strings or invalid values"""
try:
return float(value) if value else 0.0
except ValueError:
return 0.0
def safe_int(value: str) -> int:
"""Convert string to int, returning 0 for empty strings or invalid values"""
try:
return int(value) if value else 0
except ValueError:
return 0
return {
"tcgplayer_id": row.get("TCGplayer Id", ""),
"product_line": row.get("Product Line", ""),
"set_name": row.get("Set Name", ""),
"product_name": row.get("Product Name", ""),
"title": row.get("Title", ""),
"number": row.get("Number", ""),
"rarity": row.get("Rarity", ""),
"condition": row.get("Condition", ""),
"tcg_market_price": safe_float(row.get("TCG Market Price", "")),
"tcg_direct_low": safe_float(row.get("TCG Direct Low", "")),
"tcg_low_price_with_shipping": safe_float(row.get("TCG Low Price With Shipping", "")),
"tcg_low_price": safe_float(row.get("TCG Low Price", "")),
"total_quantity": safe_int(row.get("Total Quantity", "")),
"add_to_quantity": safe_int(row.get("Add to Quantity", "")),
"tcg_marketplace_price": safe_float(row.get("TCG Marketplace Price", "")),
"photo_url": row.get("Photo URL", "")
}

View File

@@ -0,0 +1,63 @@
from typing import List, Optional, Dict
from sqlalchemy.orm import Session
from app.models.inventory import Inventory
from app.services.base_service import BaseService
class InventoryService(BaseService[Inventory]):
def __init__(self):
super().__init__(Inventory)
def create(self, db: Session, obj_in: Dict) -> Inventory:
"""
Create a new inventory item in the database.
Args:
db: Database session
obj_in: Dictionary containing inventory data
Returns:
Inventory: The created inventory object
"""
return super().create(db, obj_in)
def update(self, db: Session, db_obj: Inventory, obj_in: Dict) -> Inventory:
"""
Update an existing inventory item in the database.
Args:
db: Database session
db_obj: The inventory object to update
obj_in: Dictionary containing updated inventory data
Returns:
Inventory: The updated inventory object
"""
return super().update(db, db_obj, obj_in)
def get_by_tcgplayer_id(self, db: Session, tcgplayer_id: str) -> Optional[Inventory]:
"""
Get an inventory item by its TCGPlayer ID.
Args:
db: Database session
tcgplayer_id: The TCGPlayer ID to find
Returns:
Optional[Inventory]: The inventory item if found, None otherwise
"""
return db.query(self.model).filter(self.model.tcgplayer_id == tcgplayer_id).first()
def get_by_set(self, db: Session, set_name: str, skip: int = 0, limit: int = 100) -> List[Inventory]:
"""
Get all inventory items from a specific set.
Args:
db: Database session
set_name: The name of the set to filter by
skip: Number of records to skip (for pagination)
limit: Maximum number of records to return
Returns:
List[Inventory]: List of inventory items from the specified set
"""
return db.query(self.model).filter(self.model.set_name == set_name).offset(skip).limit(limit).all()

View File

@@ -0,0 +1,58 @@
from sqlalchemy.orm import Session
from app.services.base_service import BaseService
from app.models.order import Order, OrderCard
from app.models.card import Card
class OrderService(BaseService):
def __init__(self):
super().__init__(Order)
def create_order_with_cards(self, db: Session, order_data: dict, card_ids: list[int]) -> Order:
"""
Create a new order with associated cards.
Args:
db: Database session
order_data: Dictionary containing order details
card_ids: List of card IDs to associate with the order
Returns:
The created Order object
"""
# Create the order
order = Order(**order_data)
db.add(order)
db.flush() # Get the order ID
# Associate cards with the order
for card_id in card_ids:
card = db.query(Card).filter(Card.id == card_id).first()
if not card:
raise ValueError(f"Card with ID {card_id} not found")
order_card = OrderCard(order_id=order.id, card_id=card_id)
db.add(order_card)
db.commit()
db.refresh(order)
return order
def get_orders_with_cards(self, db: Session, skip: int = 0, limit: int = 10) -> list[Order]:
"""
Get orders with their associated cards.
Args:
db: Database session
skip: Number of records to skip
limit: Maximum number of records to return
Returns:
List of Order objects with their associated cards
"""
orders = db.query(Order).offset(skip).limit(limit).all()
# Eager load the cards for each order
for order in orders:
order.cards = db.query(Card).join(OrderCard).filter(OrderCard.order_id == order.id).all()
return orders

View File

@@ -0,0 +1,54 @@
from sqlalchemy.orm import Session
from app.db.database import SessionLocal, transaction
from app.services.external_api.tcgplayer.tcgplayer_inventory_service import TCGPlayerInventoryService
from app.services.file_processing_service import FileProcessingService
from app.services.scheduler.base_scheduler import BaseScheduler
import logging
logger = logging.getLogger(__name__)
class SchedulerService:
def __init__(self):
self.tcgplayer_service = TCGPlayerInventoryService()
self.file_processor = FileProcessingService()
self.scheduler = BaseScheduler()
async def process_tcgplayer_export(self, export_type: str = "live", use_cache: bool = False):
"""
Process TCGPlayer export as a scheduled task.
Args:
export_type: Type of export to process (staged, live, or pricing)
"""
db = SessionLocal()
try:
logger.info(f"Starting scheduled TCGPlayer export processing for {export_type}")
# Download the file
file_bytes = await self.tcgplayer_service.get_tcgplayer_export(export_type)
# Process the file and load into database
with transaction(db):
stats = await self.file_processor.process_tcgplayer_export(db, export_type=export_type, file_bytes=file_bytes, use_cache=use_cache)
logger.info(f"Completed TCGPlayer export processing: {stats}")
return stats
except Exception as e:
logger.error(f"Error processing TCGPlayer export: {str(e)}")
raise
finally:
db.close()
async def start_scheduled_tasks(self):
"""Start all scheduled tasks"""
# Schedule TCGPlayer export processing to run daily at 2 AM
await self.scheduler.schedule_task(
task_name="process_tcgplayer_export",
func=self.process_tcgplayer_export,
interval_seconds=24 * 60 * 60, # 24 hours
export_type="live"
)
self.scheduler.start()
logger.info("All scheduled tasks started")