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,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