so much stuff lol
This commit is contained in:
@ -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)}")
|
||||
|
205
app/services/external_api/tcgcsv/tcgcsv_service.py
Normal file
205
app/services/external_api/tcgcsv/tcgcsv_service.py
Normal 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
|
@ -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)}")
|
||||
|
@ -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
|
@ -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
|
Reference in New Issue
Block a user