This commit is contained in:
zman 2025-04-09 21:02:43 -04:00
commit 1c00ea8569
24 changed files with 1039 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
*.db
*.db-journal
*.db-trace
*.db-wal
*.db-lock
*.db-shm
*.db-trace
__pycache__
_cookie.py

41
app/db/database.py Normal file
View File

@ -0,0 +1,41 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session
import logging
import os
from contextlib import contextmanager
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Database configuration
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///database.db")
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
"""Dependency to get a database session. Used for simple, single service operations."""
db = SessionLocal()
try:
yield db
finally:
db.close()
@contextmanager
def transaction(db: Session):
"""Context manager to handle database transactions. Used for service operations that require multiple database operations."""
try:
yield
db.commit()
except Exception as e:
db.rollback()
raise e
def init_db():
Base.metadata.create_all(bind=engine)

45
app/main.py Normal file
View File

@ -0,0 +1,45 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
import logging
from routes import routes
from db.database import init_db
from app.models.card import Card # Assuming you have a Card model
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler(), logging.FileHandler("app.log")],)
logger = logging.getLogger(__name__)
app = FastAPI(
title="CCR Cards Management API",
description="API for managing CCR Cards Inventory, Orders, and more.",
version="0.1.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(routes.router)
@app.on_event("startup")
async def on_startup():
init_db()
logger.info("Database initialized successfully")
@app.on_event("shutdown")
async def on_shutdown():
logger.info("Database connection closed")
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)

37
app/models/box.py Normal file
View File

@ -0,0 +1,37 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.database import Base
class Box(Base):
__tablename__ = "boxes"
id = Column(Integer, primary_key=True, index=True)
product_id = Column(Integer)
type = Column(String)
set_code = Column(String)
sku = Column(Integer)
name = Column(String)
expected_number_of_cards = Column(Integer)
description = Column(String)
image_url = Column(String)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
cards = relationship("Card", back_populates="box")
open_boxes = relationship("OpenBox", back_populates="box")
class OpenBox(Base):
__tablename__ = "open_boxes"
id = Column(Integer, primary_key=True, index=True)
box_id = Column(Integer, ForeignKey("boxes.id"))
number_of_cards = Column(Integer)
date_opened = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
box = relationship("Box", back_populates="open_boxes")
cards = relationship("Card", back_populates="open_box")

23
app/models/card.py Normal file
View File

@ -0,0 +1,23 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
from datetime import datetime
from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.database import Base
class Card(Base):
__tablename__ = "cards"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
rarity = Column(String)
set_name = Column(String)
price = Column(Float)
quantity = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
orders = relationship("Order", secondary="order_cards", back_populates="cards")

21
app/models/file.py Normal file
View File

@ -0,0 +1,21 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.database import Base
class File(Base):
__tablename__ = "files"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
type = Column(String)
path = Column(String)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())

20
app/models/game.py Normal file
View File

@ -0,0 +1,20 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.database import Base
class Game(Base):
__tablename__ = "games"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
description = Column(String)
image_url = Column(String)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
boxes = relationship("Box", back_populates="game")

0
app/routes/__init__.py Normal file
View File

176
app/routes/routes.py Normal file
View File

@ -0,0 +1,176 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.db.database import get_db
from app.models.file import File, FileCreate, FileUpdate, FileDelete, FileList
from app.models.box import Box, BoxCreate, BoxUpdate, BoxDelete, BoxList, OpenBox, OpenBoxCreate, OpenBoxUpdate, OpenBoxDelete, OpenBoxList
from app.models.game import Game, GameCreate, GameUpdate, GameDelete, GameList
from app.models.card import Card, CardCreate, CardUpdate, CardDelete, CardList
from app.services import CardService, OrderService
router = APIRouter(prefix="/api")
# Initialize services
card_service = CardService()
order_service = OrderService()
# ============================================================================
# Health Check & Root Endpoints
# ============================================================================
@router.get("/")
async def root():
return {"message": "CCR Cards Management API is running."}
@router.get("/health")
async def health():
return {"status": "ok"}
# ============================================================================
# Card Management Endpoints
# ============================================================================
@router.get("/cards", response_model=CardList)
async def get_cards(
db: Session = Depends(get_db),
page: int = 1,
limit: int = 10,
type: str = None,
id: int = None
):
skip = (page - 1) * limit
cards = card_service.get_all(db, skip=skip, limit=limit)
total = db.query(Card).count()
return {
"cards": cards,
"total": total,
"page": page,
"limit": limit
}
@router.post("/cards", response_model=Card)
async def create_card(card: CardCreate, db: Session = Depends(get_db)):
try:
created_card = card_service.create(db, card.dict())
return created_card
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/cards/{card_id}", response_model=Card)
async def update_card(card_id: int, card: CardUpdate, db: Session = Depends(get_db)):
db_card = card_service.get(db, card_id)
if not db_card:
raise HTTPException(status_code=404, detail="Card not found")
try:
updated_card = card_service.update(db, db_card, card.dict(exclude_unset=True))
return updated_card
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/cards/{card_id}", response_model=CardDelete)
async def delete_card(card_id: int, db: Session = Depends(get_db)):
success = card_service.delete(db, card_id)
if not success:
raise HTTPException(status_code=404, detail="Card not found")
return {"message": "Card deleted successfully"}
# ============================================================================
# Order Management Endpoints
# ============================================================================
@router.post("/orders")
async def create_order(order_data: dict, card_ids: list[int], db: Session = Depends(get_db)):
try:
order = order_service.create_order_with_cards(db, order_data, card_ids)
return order
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/orders")
async def get_orders(
db: Session = Depends(get_db),
page: int = 1,
limit: int = 10
):
skip = (page - 1) * limit
orders = order_service.get_orders_with_cards(db, skip=skip, limit=limit)
return {
"orders": orders,
"page": page,
"limit": limit
}
# ============================================================================
# File Management Endpoints
# ============================================================================
@router.get("/files", response_model=FileList)
async def get_files(page: int = 1, limit: int = 10, type: str = None, id: int = None):
return {"files": [], "total": 0, "page": page, "limit": limit}
@router.post("/files", response_model=File)
async def create_file(file: FileCreate):
return {"message": "File created successfully"}
@router.put("/files/{file_id}", response_model=File)
async def update_file(file_id: int, file: FileUpdate):
return {"message": "File updated successfully"}
@router.delete("/files/{file_id}", response_model=FileDelete)
async def delete_file(file_id: int):
return {"message": "File deleted successfully"}
# ============================================================================
# Box Management Endpoints
# ============================================================================
@router.get("/boxes", response_model=BoxList)
async def get_boxes(page: int = 1, limit: int = 10, type: str = None, id: int = None):
return {"boxes": [], "total": 0, "page": page, "limit": limit}
@router.post("/boxes", response_model=Box)
async def create_box(box: BoxCreate):
return {"message": "Box created successfully"}
@router.put("/boxes/{box_id}", response_model=Box)
async def update_box(box_id: int, box: BoxUpdate):
return {"message": "Box updated successfully"}
@router.delete("/boxes/{box_id}", response_model=BoxDelete)
async def delete_box(box_id: int):
return {"message": "Box deleted successfully"}
# ============================================================================
# Open Box Management Endpoints
# ============================================================================
@router.get("/open_boxes", response_model=OpenBoxList)
async def get_open_boxes(page: int = 1, limit: int = 10, type: str = None, id: int = None):
return {"open_boxes": [], "total": 0, "page": page, "limit": limit}
@router.post("/open_boxes", response_model=OpenBox)
async def create_open_box(open_box: OpenBoxCreate):
return {"message": "Open box created successfully"}
@router.put("/open_boxes/{open_box_id}", response_model=OpenBox)
async def update_open_box(open_box_id: int, open_box: OpenBoxUpdate):
return {"message": "Open box updated successfully"}
@router.delete("/open_boxes/{open_box_id}", response_model=OpenBoxDelete)
async def delete_open_box(open_box_id: int):
return {"message": "Open box deleted successfully"}
# ============================================================================
# Game Management Endpoints
# ============================================================================
@router.get("/games", response_model=GameList)
async def get_games(page: int = 1, limit: int = 10, type: str = None, id: int = None):
return {"games": [], "total": 0, "page": page, "limit": limit}
@router.post("/games", response_model=Game)
async def create_game(game: GameCreate):
return {"message": "Game created successfully"}
@router.put("/games/{game_id}", response_model=Game)
async def update_game(game_id: int, game: GameUpdate):
return {"message": "Game updated successfully"}
@router.delete("/games/{game_id}", response_model=GameDelete)
async def delete_game(game_id: int):
return {"message": "Game deleted successfully"}

81
app/schemas/box.py Normal file
View File

@ -0,0 +1,81 @@
from pydantic import BaseModel, ConfigDict
from typing import Optional, List
from datetime import datetime
# Base schema with common attributes
class BoxBase(BaseModel):
product_id: int
type: str
set_code: str
sku: int
name: str
expected_number_of_cards: int
description: str
image_url: str
# Schema for creating a new box
class BoxCreate(BoxBase):
pass
# Schema for updating a box
class BoxUpdate(BaseModel):
product_id: Optional[int] = None
type: Optional[str] = None
set_code: Optional[str] = None
sku: Optional[int] = None
name: Optional[str] = None
expected_number_of_cards: Optional[int] = None
description: Optional[str] = None
image_url: Optional[str] = None
# Schema for reading a box
class Box(BoxBase):
id: int
created_at: datetime
updated_at: datetime
cards: List["Card"] = []
open_boxes: List["OpenBox"] = []
model_config = ConfigDict(from_attributes=True)
# Schema for deleting a box
class BoxDelete(BaseModel):
id: int
# Schema for listing boxes
class BoxList(BaseModel):
boxes: List[Box]
total: int
page: int
limit: int
# OpenBox schemas
class OpenBoxBase(BaseModel):
box_id: int
number_of_cards: int
date_opened: datetime
class OpenBoxCreate(OpenBoxBase):
pass
class OpenBoxUpdate(BaseModel):
number_of_cards: Optional[int] = None
date_opened: Optional[datetime] = None
class OpenBox(OpenBoxBase):
id: int
created_at: datetime
updated_at: datetime
box: Optional[Box] = None
cards: List["Card"] = []
model_config = ConfigDict(from_attributes=True)
class OpenBoxDelete(BaseModel):
id: int
class OpenBoxList(BaseModel):
open_boxes: List[OpenBox]
total: int
page: int
limit: int

39
app/schemas/card.py Normal file
View File

@ -0,0 +1,39 @@
from pydantic import BaseModel, ConfigDict
from typing import Optional, List
from datetime import datetime
# Base schema with common attributes
class CardBase(BaseModel):
name: str
rarity: str
set_name: str
price: float
quantity: int = 0
# Schema for creating a new card
class CardCreate(CardBase):
pass
# Schema for updating a card
class CardUpdate(BaseModel):
name: Optional[str] = None
rarity: Optional[str] = None
set_name: Optional[str] = None
price: Optional[float] = None
quantity: Optional[int] = None
# Schema for reading a card (includes id and relationships)
class Card(CardBase):
id: int
created_at: datetime
updated_at: datetime
orders: List["Order"] = []
model_config = ConfigDict(from_attributes=True)
# Schema for listing cards
class CardList(BaseModel):
cards: List[Card]
total: int
page: int
limit: int

38
app/schemas/file.py Normal file
View File

@ -0,0 +1,38 @@
from pydantic import BaseModel, ConfigDict
from typing import Optional, List
from datetime import datetime
# Base schema with common attributes
class FileBase(BaseModel):
name: str
type: str
path: str
# Schema for creating a new file
class FileCreate(FileBase):
pass
# Schema for updating a file
class FileUpdate(BaseModel):
name: Optional[str] = None
type: Optional[str] = None
path: Optional[str] = None
# Schema for reading a file
class File(FileBase):
id: int
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
# Schema for deleting a file
class FileDelete(BaseModel):
id: int
# Schema for listing files
class FileList(BaseModel):
files: List[File]
total: int
page: int
limit: int

39
app/schemas/game.py Normal file
View File

@ -0,0 +1,39 @@
from pydantic import BaseModel, ConfigDict
from typing import Optional, List
from datetime import datetime
# Base schema with common attributes
class GameBase(BaseModel):
name: str
description: str
image_url: str
# Schema for creating a new game
class GameCreate(GameBase):
pass
# Schema for updating a game
class GameUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
image_url: Optional[str] = None
# Schema for reading a game
class Game(GameBase):
id: int
created_at: datetime
updated_at: datetime
boxes: List["Box"] = []
model_config = ConfigDict(from_attributes=True)
# Schema for deleting a game
class GameDelete(BaseModel):
id: int
# Schema for listing games
class GameList(BaseModel):
games: List[Game]
total: int
page: int
limit: int

5
app/services/__init__.py Normal file
View 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"]

View 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)

View 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()

View 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()

View File

@ -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()

View 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()

View File

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

View 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

View 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")

View 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()