kind of a mess lol but file caching and front end
This commit is contained in:
@ -2,6 +2,11 @@ from typing import Any, Dict, Optional, Union
|
||||
import aiohttp
|
||||
import logging
|
||||
import json
|
||||
import csv
|
||||
import io
|
||||
from app.services.service_manager import ServiceManager
|
||||
from app.schemas.file import FileInDB
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -10,6 +15,8 @@ class BaseExternalService:
|
||||
self.base_url = base_url
|
||||
self.api_key = api_key
|
||||
self.session = None
|
||||
self.service_manager = ServiceManager()
|
||||
self._services = {}
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self.session is None or self.session.closed:
|
||||
@ -73,4 +80,36 @@ class BaseExternalService:
|
||||
if self.session and not self.session.closed:
|
||||
await self.session.close()
|
||||
self.session = None
|
||||
logger.info(f"Closed session for {self.__class__.__name__}")
|
||||
logger.info(f"Closed session for {self.__class__.__name__}")
|
||||
|
||||
def get_service(self, name: str) -> Any:
|
||||
"""Get a service by name with lazy loading"""
|
||||
if name not in self._services:
|
||||
self._services[name] = self.service_manager.get_service(name)
|
||||
return self._services[name]
|
||||
|
||||
@property
|
||||
def file_service(self):
|
||||
"""Convenience property for file service"""
|
||||
return self.get_service('file')
|
||||
|
||||
async def save_file(self, db: Session, file_data: Union[bytes, list[dict]], file_name: str, subdir: str, file_type: Optional[str] = None) -> FileInDB:
|
||||
"""Save a file using the FileService"""
|
||||
if isinstance(file_data, list):
|
||||
# Convert list of dictionaries to CSV bytes
|
||||
output = io.StringIO()
|
||||
writer = csv.DictWriter(output, fieldnames=file_data[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(file_data)
|
||||
file_data = output.getvalue().encode('utf-8')
|
||||
file_type = file_type or 'text/csv'
|
||||
|
||||
# Use FileService to save the file
|
||||
file_service = self.get_service('file')
|
||||
return await file_service.save_file(
|
||||
db=db,
|
||||
file_data=file_data,
|
||||
filename=file_name,
|
||||
subdir=subdir,
|
||||
file_type=file_type
|
||||
)
|
@ -11,9 +11,12 @@ from datetime import datetime
|
||||
from app.models.mtgjson_card import MTGJSONCard
|
||||
from app.models.mtgjson_sku import MTGJSONSKU
|
||||
from app.db.database import get_db, transaction
|
||||
from app.services.external_api.base_external_service import BaseExternalService
|
||||
from app.schemas.file import FileInDB
|
||||
|
||||
class MTGJSONService:
|
||||
class MTGJSONService(BaseExternalService):
|
||||
def __init__(self, cache_dir: str = "app/data/cache/mtgjson", batch_size: int = 1000):
|
||||
super().__init__(base_url="https://mtgjson.com/api/v5/")
|
||||
self.cache_dir = cache_dir
|
||||
self.identifiers_dir = os.path.join(cache_dir, "identifiers")
|
||||
self.skus_dir = os.path.join(cache_dir, "skus")
|
||||
@ -38,27 +41,22 @@ class MTGJSONService:
|
||||
"""Print progress message with flush"""
|
||||
print(message, end=end, flush=True)
|
||||
|
||||
async def _download_file(self, url: str, output_path: str) -> None:
|
||||
"""Download a file from the given URL to the specified path using streaming"""
|
||||
async def _download_file(self, db: Session, url: str, filename: str, subdir: str) -> FileInDB:
|
||||
"""Download a file from the given URL and save it using FileService"""
|
||||
print(f"Downloading {url}...")
|
||||
start_time = time.time()
|
||||
total_size = 0
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
with open(output_path, 'wb') as f:
|
||||
downloaded = 0
|
||||
async for chunk in response.content.iter_chunked(8192):
|
||||
f.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
if total_size > 0:
|
||||
percent = (downloaded / total_size) * 100
|
||||
elapsed = time.time() - start_time
|
||||
speed = downloaded / elapsed / 1024 / 1024 # MB/s
|
||||
print(f"\rDownloading: {percent:.1f}% ({downloaded/1024/1024:.1f}MB/{total_size/1024/1024:.1f}MB) at {speed:.1f}MB/s", end="")
|
||||
print("\nDownload complete!")
|
||||
file_data = await response.read()
|
||||
return await self.save_file(
|
||||
db=db,
|
||||
file_data=file_data,
|
||||
file_name=filename,
|
||||
subdir=f"mtgjson/{subdir}",
|
||||
file_type=response.headers.get('content-type', 'application/octet-stream')
|
||||
)
|
||||
else:
|
||||
raise Exception(f"Failed to download file from {url}. Status: {response.status}")
|
||||
|
||||
@ -153,14 +151,16 @@ class MTGJSONService:
|
||||
self._print_progress("Starting MTGJSON identifiers processing...")
|
||||
start_time = time.time()
|
||||
|
||||
zip_path = os.path.join(self.identifiers_dir, "AllIdentifiers.json.zip")
|
||||
await self._download_file(
|
||||
"https://mtgjson.com/api/v5/AllIdentifiers.json.zip",
|
||||
zip_path
|
||||
# Download the file using FileService
|
||||
file_record = await self._download_file(
|
||||
db=db,
|
||||
url="https://mtgjson.com/api/v5/AllIdentifiers.json.zip",
|
||||
filename="AllIdentifiers.json.zip",
|
||||
subdir="identifiers"
|
||||
)
|
||||
|
||||
self._print_progress("Unzipping file...")
|
||||
json_path = await self._unzip_file(zip_path, self.identifiers_dir)
|
||||
|
||||
# Get the file path from the database record
|
||||
zip_path = file_record.path
|
||||
|
||||
cards_processed = 0
|
||||
current_batch = []
|
||||
@ -169,7 +169,7 @@ class MTGJSONService:
|
||||
|
||||
self._print_progress("Processing cards...")
|
||||
try:
|
||||
for item in self._stream_json_file(json_path):
|
||||
for item in self._stream_json_file(zip_path):
|
||||
if item["type"] == "meta":
|
||||
self._print_progress(f"Processing MTGJSON data version {item['data'].get('version')} from {item['data'].get('date')}")
|
||||
continue
|
||||
@ -239,14 +239,16 @@ class MTGJSONService:
|
||||
self._print_progress("Starting MTGJSON SKUs processing...")
|
||||
start_time = time.time()
|
||||
|
||||
zip_path = os.path.join(self.skus_dir, "TcgplayerSkus.json.zip")
|
||||
await self._download_file(
|
||||
"https://mtgjson.com/api/v5/TcgplayerSkus.json.zip",
|
||||
zip_path
|
||||
# Download the file using FileService
|
||||
file_record = await self._download_file(
|
||||
db=db,
|
||||
url="https://mtgjson.com/api/v5/TcgplayerSkus.json.zip",
|
||||
filename="TcgplayerSkus.json.zip",
|
||||
subdir="skus"
|
||||
)
|
||||
|
||||
self._print_progress("Unzipping file...")
|
||||
json_path = await self._unzip_file(zip_path, self.skus_dir)
|
||||
|
||||
# Get the file path from the database record
|
||||
zip_path = file_record.path
|
||||
|
||||
skus_processed = 0
|
||||
current_batch = []
|
||||
@ -255,7 +257,7 @@ class MTGJSONService:
|
||||
|
||||
self._print_progress("Processing SKUs...")
|
||||
try:
|
||||
for item in self._stream_json_file(json_path):
|
||||
for item in self._stream_json_file(zip_path):
|
||||
if item["type"] == "meta":
|
||||
self._print_progress(f"Processing MTGJSON SKUs version {item['data'].get('version')} from {item['data'].get('date')}")
|
||||
continue
|
||||
|
@ -10,6 +10,7 @@ from app.db.database import get_db, transaction
|
||||
from sqlalchemy.orm import Session
|
||||
import py7zr
|
||||
import os
|
||||
from app.schemas.file import FileInDB
|
||||
|
||||
class TCGCSVService(BaseExternalService):
|
||||
def __init__(self):
|
||||
@ -37,32 +38,28 @@ class TCGCSVService(BaseExternalService):
|
||||
endpoint = "tcgplayer/categories"
|
||||
return await self._make_request("GET", endpoint)
|
||||
|
||||
async def get_archived_prices_for_date(self, date_str: str):
|
||||
async def get_archived_prices_for_date(self, db: Session, date_str: str) -> str:
|
||||
"""Fetch archived prices from TCGCSV API"""
|
||||
# Check if the date directory already exists
|
||||
extract_path = f"app/data/cache/tcgcsv/prices/{date_str}"
|
||||
if os.path.exists(extract_path):
|
||||
print(f"Prices for date {date_str} already exist, skipping download")
|
||||
return date_str
|
||||
|
||||
# Download the archive file
|
||||
endpoint = f"archive/tcgplayer/prices-{date_str}.ppmd.7z"
|
||||
response = await self._make_request("GET", endpoint, binary=True)
|
||||
|
||||
# Save the archive file
|
||||
archive_path = f"app/data/cache/tcgcsv/prices/zip/prices-{date_str}.ppmd.7z"
|
||||
os.makedirs(os.path.dirname(archive_path), exist_ok=True)
|
||||
with open(archive_path, "wb") as f:
|
||||
f.write(response)
|
||||
|
||||
# Save the archive file using FileService
|
||||
file_record = await self.save_file(
|
||||
db=db,
|
||||
file_data=response,
|
||||
file_name=f"prices-{date_str}.ppmd.7z",
|
||||
subdir=f"tcgcsv/prices/zip",
|
||||
file_type="application/x-7z-compressed"
|
||||
)
|
||||
|
||||
# Extract the 7z file
|
||||
with py7zr.SevenZipFile(archive_path, 'r') as archive:
|
||||
with py7zr.SevenZipFile(file_record.path, 'r') as archive:
|
||||
# Extract to a directory named after the date
|
||||
extract_path = f"app/data/cache/tcgcsv/prices/{date_str}"
|
||||
os.makedirs(extract_path, exist_ok=True)
|
||||
archive.extractall(path=extract_path)
|
||||
|
||||
# The extracted files will be in a directory structure like:
|
||||
# {date_str}/{game_id}/{group_id}/prices
|
||||
return date_str
|
||||
|
||||
async def get_archived_prices_for_date_range(self, start_date: str, end_date: str):
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from typing import Any, Dict, Optional, Union, Literal
|
||||
import logging
|
||||
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
|
||||
from app.schemas.tcgplayer import (
|
||||
@ -21,7 +21,8 @@ from app.db.database import transaction
|
||||
import os
|
||||
import csv
|
||||
import io
|
||||
|
||||
from app.schemas.file import FileInDB
|
||||
from datetime import datetime
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OrderManagementService(BaseTCGPlayerService):
|
||||
@ -87,7 +88,12 @@ class OrderManagementService(BaseTCGPlayerService):
|
||||
response = await self._make_request("GET", f"/{order_id}{self.API_VERSION}")
|
||||
return response
|
||||
|
||||
async def get_packing_slip(self, order_ids: list[str]):
|
||||
async def get_or_create_packing_slip(self, db: Session, order_ids: list[str]) -> FileInDB:
|
||||
# check if the file already exists
|
||||
file_service = self.get_service('file')
|
||||
file = await file_service.get_file_by_metadata(db, "order_ids", order_ids, "packing_slip", "application/pdf")
|
||||
if file:
|
||||
return file
|
||||
payload = {
|
||||
"sortingType": "byRelease",
|
||||
"format": "default",
|
||||
@ -95,40 +101,53 @@ class OrderManagementService(BaseTCGPlayerService):
|
||||
"orderNumbers": order_ids
|
||||
}
|
||||
response = await self._make_request("POST", self.packing_slip_endpoint, data=payload, headers=self._get_headers("POST", "application/json"), auth_required=True, download_file=True)
|
||||
return response
|
||||
return await file_service.save_file(
|
||||
db=db,
|
||||
file_data=response,
|
||||
filename=f"packing_slip_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.pdf",
|
||||
subdir='tcgplayer/packing_slips/pdf',
|
||||
file_type='packing_slip',
|
||||
content_type='application/pdf',
|
||||
metadata={"order_ids": order_ids}
|
||||
)
|
||||
|
||||
async def get_pull_sheet(self, order_ids: list[str]):
|
||||
async def get_pull_sheet(self, db: Session, order_ids: list[str]) -> FileInDB:
|
||||
payload = {
|
||||
"orderNumbers": order_ids,
|
||||
"timezoneOffset": -4
|
||||
}
|
||||
response = await self._make_request("POST", self.pull_sheet_endpoint, data=payload, headers=self._get_headers("POST", "application/json"), auth_required=True, download_file=True)
|
||||
return response
|
||||
# get file service
|
||||
file_service = self.get_service('file')
|
||||
# save file
|
||||
return await file_service.save_file(
|
||||
db=db,
|
||||
file_data=response,
|
||||
filename=f"pull_sheet_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv",
|
||||
subdir='tcgplayer/pull_sheets/csv',
|
||||
file_type='pull_sheet',
|
||||
content_type='text/csv',
|
||||
metadata={"order_ids": order_ids}
|
||||
)
|
||||
|
||||
async def get_shipping_csv(self, order_ids: list[str]):
|
||||
async def get_shipping_csv(self, db: Session, order_ids: list[str]) -> FileInDB:
|
||||
payload = {
|
||||
"orderNumbers": order_ids,
|
||||
"timezoneOffset": -4
|
||||
}
|
||||
response = await self._make_request("POST", self.shipping_endpoint, data=payload, headers=self._get_headers("POST", "application/json"), auth_required=True, download_file=True)
|
||||
return response
|
||||
|
||||
async def save_file(self, file_data: Union[bytes, list[dict]], file_name: str) -> str:
|
||||
if not os.path.exists("app/data/cache/tcgplayer/orders"):
|
||||
os.makedirs("app/data/cache/tcgplayer/orders")
|
||||
file_path = f"app/data/cache/tcgplayer/orders/{file_name}"
|
||||
|
||||
if isinstance(file_data, list):
|
||||
# Convert list of dictionaries to CSV bytes
|
||||
output = io.StringIO()
|
||||
writer = csv.DictWriter(output, fieldnames=file_data[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(file_data)
|
||||
file_data = output.getvalue().encode('utf-8')
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(file_data)
|
||||
return file_path
|
||||
# get file service
|
||||
file_service = self.get_service('file')
|
||||
# save file
|
||||
return await file_service.save_file(
|
||||
db=db,
|
||||
file_data=response,
|
||||
filename=f"shipping_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv",
|
||||
subdir='tcgplayer/shipping/csv',
|
||||
file_type='shipping_csv',
|
||||
content_type='text/csv',
|
||||
metadata={"order_ids": order_ids}
|
||||
)
|
||||
|
||||
async def save_order_to_db(self, order: dict, db: Session):
|
||||
# Parse API response using our API schema
|
||||
|
@ -1,21 +1,33 @@
|
||||
from typing import Dict, List, Optional
|
||||
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
|
||||
from sqlalchemy.orm import Session
|
||||
from app.schemas.file import FileInDB
|
||||
|
||||
class TCGPlayerInventoryService(BaseTCGPlayerService):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
async def get_tcgplayer_export(self, export_type: str):
|
||||
async def get_tcgplayer_export(self, db: Session, export_type: str) -> FileInDB:
|
||||
"""
|
||||
Get a TCGPlayer Staged Inventory Export, Live Inventory Export, or Pricing Export
|
||||
"""
|
||||
if export_type == "staged":
|
||||
endpoint = self.staged_inventory_endpoint
|
||||
file_type = "text/csv"
|
||||
elif export_type == "live":
|
||||
endpoint = self.live_inventory_endpoint
|
||||
file_type = "text/csv"
|
||||
elif export_type == "pricing":
|
||||
endpoint = self.pricing_export_endpoint
|
||||
file_type = "text/csv"
|
||||
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
|
||||
return await self.save_file(
|
||||
db=db,
|
||||
file_data=file_bytes,
|
||||
file_name=f"tcgplayer_{export_type}_export.csv",
|
||||
subdir="tcgplayer/inventory",
|
||||
file_type=file_type
|
||||
)
|
Reference in New Issue
Block a user