sex
This commit is contained in:
5
app/services/__init__.py
Normal file
5
app/services/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from app.services.base_service import BaseService
|
||||
from app.services.card_service import CardService
|
||||
from app.services.service_registry import ServiceRegistry
|
||||
|
||||
__all__ = ["BaseService", "CardService", "ServiceRegistry"]
|
45
app/services/base_service.py
Normal file
45
app/services/base_service.py
Normal file
@ -0,0 +1,45 @@
|
||||
from typing import Type, TypeVar, Generic, List, Optional, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import Base
|
||||
from app.services.service_registry import ServiceRegistry
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class BaseService(Generic[T]):
|
||||
def __init__(self, model: Type[T]):
|
||||
self.model = model
|
||||
# Register the service instance
|
||||
ServiceRegistry.register(self.__class__.__name__, self)
|
||||
|
||||
def get(self, db: Session, id: int) -> Optional[T]:
|
||||
return db.query(self.model).filter(self.model.id == id).first()
|
||||
|
||||
def get_all(self, db: Session, skip: int = 0, limit: int = 100) -> List[T]:
|
||||
return db.query(self.model).offset(skip).limit(limit).all()
|
||||
|
||||
def create(self, db: Session, obj_in: dict) -> T:
|
||||
db_obj = self.model(**obj_in)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def update(self, db: Session, db_obj: T, obj_in: dict) -> T:
|
||||
for field in obj_in:
|
||||
if hasattr(db_obj, field):
|
||||
setattr(db_obj, field, obj_in[field])
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
def delete(self, db: Session, id: int) -> bool:
|
||||
obj = db.query(self.model).filter(self.model.id == id).first()
|
||||
if obj:
|
||||
db.delete(obj)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_service(self, service_name: str) -> Any:
|
||||
return ServiceRegistry.get(service_name)
|
17
app/services/card_service.py
Normal file
17
app/services/card_service.py
Normal file
@ -0,0 +1,17 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from app.services.base_service import BaseService
|
||||
from app.models.card import Card
|
||||
|
||||
class CardService(BaseService[Card]):
|
||||
def __init__(self):
|
||||
super().__init__(Card)
|
||||
|
||||
def get_by_name(self, db: Session, name: str) -> Optional[Card]:
|
||||
return db.query(self.model).filter(self.model.name == name).first()
|
||||
|
||||
def get_by_rarity(self, db: Session, rarity: str, skip: int = 0, limit: int = 100) -> List[Card]:
|
||||
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()
|
49
app/services/external_api/base_external_service.py
Normal file
49
app/services/external_api/base_external_service.py
Normal file
@ -0,0 +1,49 @@
|
||||
from typing import Any, Dict, Optional
|
||||
import aiohttp
|
||||
import logging
|
||||
from app.services.service_registry import ServiceRegistry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BaseExternalService:
|
||||
def __init__(self, base_url: str, api_key: Optional[str] = None):
|
||||
self.base_url = base_url
|
||||
self.api_key = api_key
|
||||
self.session = None
|
||||
# Register the service instance
|
||||
ServiceRegistry.register(self.__class__.__name__, self)
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self.session is None or self.session.closed:
|
||||
self.session = aiohttp.ClientSession()
|
||||
return self.session
|
||||
|
||||
async def _make_request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
session = await self._get_session()
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
if self.api_key:
|
||||
headers = headers or {}
|
||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||
|
||||
try:
|
||||
async with session.request(method, url, params=params, headers=headers, json=data) as response:
|
||||
response.raise_for_status()
|
||||
return await response.json()
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"API request failed: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during API request: {str(e)}")
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
if self.session and not self.session.closed:
|
||||
await self.session.close()
|
@ -0,0 +1,95 @@
|
||||
from typing import Any, Dict, Optional
|
||||
import aiohttp
|
||||
import logging
|
||||
from app.services.external_api.base_external_service import BaseExternalService
|
||||
from app.services.external_api.tcgplayer.tcgplayer_credentials import TCGPlayerCredentials
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BaseTCGPlayerService(BaseExternalService):
|
||||
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"
|
||||
)
|
||||
self.credentials = TCGPlayerCredentials()
|
||||
|
||||
def _get_headers(self, method: str) -> Dict[str, str]:
|
||||
"""Get headers based on the HTTP method"""
|
||||
base_headers = {
|
||||
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||
'accept-language': 'en-US,en;q=0.8',
|
||||
'priority': 'u=0, i',
|
||||
'referer': 'https://store.tcgplayer.com/admin/pricing',
|
||||
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Brave";v="132"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"macOS"',
|
||||
'sec-fetch-dest': 'document',
|
||||
'sec-fetch-mode': 'navigate',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'sec-fetch-user': '?1',
|
||||
'sec-gpc': '1',
|
||||
'upgrade-insecure-requests': '1',
|
||||
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36'
|
||||
}
|
||||
|
||||
if method == 'POST':
|
||||
post_headers = {
|
||||
'cache-control': 'max-age=0',
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
'origin': 'https://store.tcgplayer.com'
|
||||
}
|
||||
base_headers.update(post_headers)
|
||||
|
||||
return base_headers
|
||||
|
||||
async def _make_request(
|
||||
self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
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]:
|
||||
session = await self._get_session()
|
||||
url = f"{self.store_base_url}{endpoint}"
|
||||
|
||||
# Get the authentication cookie if required
|
||||
if auth_required:
|
||||
cookie = self.credentials.get_cookie()
|
||||
if not cookie:
|
||||
raise RuntimeError("TCGPlayer authentication cookie not set. Please set the cookie using TCGPlayerCredentials.set_cookie()")
|
||||
|
||||
# Get method-specific headers and update with any provided headers
|
||||
request_headers = self._get_headers(method)
|
||||
if headers:
|
||||
request_headers.update(headers)
|
||||
request_headers["Cookie"] = cookie
|
||||
else:
|
||||
request_headers = headers or {}
|
||||
|
||||
try:
|
||||
async with session.request(method, url, params=params, headers=request_headers, json=data) as response:
|
||||
if response.status == 401:
|
||||
raise RuntimeError("TCGPlayer authentication failed. Cookie may be invalid or expired.")
|
||||
response.raise_for_status()
|
||||
return await response.json()
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"TCGPlayer API request failed: {str(e)}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during TCGPlayer API request: {str(e)}")
|
||||
raise
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
if self.session is None or self.session.closed:
|
||||
self.session = aiohttp.ClientSession()
|
||||
return self.session
|
||||
|
||||
async def close(self):
|
||||
if self.session and not self.session.closed:
|
||||
await self.session.close()
|
61
app/services/external_api/tcgplayer/tcgplayer_credentials.py
Normal file
61
app/services/external_api/tcgplayer/tcgplayer_credentials.py
Normal file
@ -0,0 +1,61 @@
|
||||
import os
|
||||
import json
|
||||
from typing import Optional
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TCGPlayerCredentials:
|
||||
_instance = None
|
||||
_credentials_file = Path.home() / ".tcgplayer" / "credentials.json"
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(TCGPlayerCredentials, cls).__new__(cls)
|
||||
cls._instance._initialize()
|
||||
return cls._instance
|
||||
|
||||
def _initialize(self):
|
||||
"""Initialize the credentials manager"""
|
||||
self._cookie = None
|
||||
self._load_credentials()
|
||||
|
||||
def _load_credentials(self):
|
||||
"""Load credentials from the credentials file"""
|
||||
try:
|
||||
if self._credentials_file.exists():
|
||||
with open(self._credentials_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
self._cookie = data.get('cookie')
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading TCGPlayer credentials: {str(e)}")
|
||||
|
||||
def _save_credentials(self):
|
||||
"""Save credentials to the credentials file"""
|
||||
try:
|
||||
# Create directory if it doesn't exist
|
||||
self._credentials_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(self._credentials_file, 'w') as f:
|
||||
json.dump({'cookie': self._cookie}, f)
|
||||
|
||||
# Set appropriate file permissions
|
||||
self._credentials_file.chmod(0o600)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving TCGPlayer credentials: {str(e)}")
|
||||
|
||||
def set_cookie(self, cookie: str):
|
||||
"""Set the authentication cookie"""
|
||||
self._cookie = cookie
|
||||
self._save_credentials()
|
||||
|
||||
def get_cookie(self) -> Optional[str]:
|
||||
"""Get the authentication cookie"""
|
||||
return self._cookie
|
||||
|
||||
def clear_credentials(self):
|
||||
"""Clear stored credentials"""
|
||||
self._cookie = None
|
||||
if self._credentials_file.exists():
|
||||
self._credentials_file.unlink()
|
@ -0,0 +1,18 @@
|
||||
from typing import Dict, List, Optional
|
||||
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
|
||||
|
||||
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
|
106
app/services/external_api/tcgplayer/tcgplayer_order_service.py
Normal file
106
app/services/external_api/tcgplayer/tcgplayer_order_service.py
Normal file
@ -0,0 +1,106 @@
|
||||
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
|
55
app/services/scheduler/base_scheduler.py
Normal file
55
app/services/scheduler/base_scheduler.py
Normal file
@ -0,0 +1,55 @@
|
||||
from typing import Callable, Dict, Any
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
import logging
|
||||
from app.services.service_registry import ServiceRegistry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BaseScheduler:
|
||||
def __init__(self):
|
||||
self.scheduler = AsyncIOScheduler()
|
||||
self.jobs: Dict[str, Any] = {}
|
||||
ServiceRegistry.register(self.__class__.__name__, self)
|
||||
|
||||
async def schedule_task(
|
||||
self,
|
||||
task_name: str,
|
||||
func: Callable,
|
||||
interval_seconds: int,
|
||||
*args,
|
||||
**kwargs
|
||||
) -> None:
|
||||
"""Schedule a task to run at regular intervals using APScheduler"""
|
||||
if task_name in self.jobs:
|
||||
logger.warning(f"Task {task_name} already exists. Removing existing job.")
|
||||
self.jobs[task_name].remove()
|
||||
|
||||
job = self.scheduler.add_job(
|
||||
func,
|
||||
trigger=IntervalTrigger(seconds=interval_seconds),
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
id=task_name,
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
self.jobs[task_name] = job
|
||||
logger.info(f"Scheduled task {task_name} to run every {interval_seconds} seconds")
|
||||
|
||||
def remove_task(self, task_name: str) -> None:
|
||||
"""Remove a scheduled task"""
|
||||
if task_name in self.jobs:
|
||||
self.jobs[task_name].remove()
|
||||
del self.jobs[task_name]
|
||||
logger.info(f"Removed task {task_name}")
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the scheduler"""
|
||||
self.scheduler.start()
|
||||
logger.info("Scheduler started")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the scheduler"""
|
||||
self.scheduler.shutdown()
|
||||
logger.info("Scheduler stopped")
|
18
app/services/service_registry.py
Normal file
18
app/services/service_registry.py
Normal file
@ -0,0 +1,18 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class ServiceRegistry:
|
||||
_services: Dict[str, Any] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, name: str, service: Any) -> None:
|
||||
cls._services[name] = service
|
||||
|
||||
@classmethod
|
||||
def get(cls, name: str) -> Any:
|
||||
if name not in cls._services:
|
||||
raise ValueError(f"Service {name} not found in registry")
|
||||
return cls._services[name]
|
||||
|
||||
@classmethod
|
||||
def clear(cls) -> None:
|
||||
cls._services.clear()
|
Reference in New Issue
Block a user