commit 1c00ea8569f33a6635ce292bb422a85891f73734 Author: zman Date: Wed Apr 9 21:02:43 2025 -0400 sex diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af5f038 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.db +*.db-journal +*.db-trace +*.db-wal +*.db-lock +*.db-shm +*.db-trace + +__pycache__ +_cookie.py diff --git a/app/db/database.py b/app/db/database.py new file mode 100644 index 0000000..19a6744 --- /dev/null +++ b/app/db/database.py @@ -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) + diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..f38f8d6 --- /dev/null +++ b/app/main.py @@ -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) diff --git a/app/models/box.py b/app/models/box.py new file mode 100644 index 0000000..1a6afa2 --- /dev/null +++ b/app/models/box.py @@ -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") diff --git a/app/models/card.py b/app/models/card.py new file mode 100644 index 0000000..ce379c5 --- /dev/null +++ b/app/models/card.py @@ -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") diff --git a/app/models/file.py b/app/models/file.py new file mode 100644 index 0000000..87c094f --- /dev/null +++ b/app/models/file.py @@ -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()) + + + diff --git a/app/models/game.py b/app/models/game.py new file mode 100644 index 0000000..b757a57 --- /dev/null +++ b/app/models/game.py @@ -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") diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/routes.py b/app/routes/routes.py new file mode 100644 index 0000000..2400554 --- /dev/null +++ b/app/routes/routes.py @@ -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"} + diff --git a/app/schemas/box.py b/app/schemas/box.py new file mode 100644 index 0000000..e8701ae --- /dev/null +++ b/app/schemas/box.py @@ -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 \ No newline at end of file diff --git a/app/schemas/card.py b/app/schemas/card.py new file mode 100644 index 0000000..b120155 --- /dev/null +++ b/app/schemas/card.py @@ -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 \ No newline at end of file diff --git a/app/schemas/file.py b/app/schemas/file.py new file mode 100644 index 0000000..671373a --- /dev/null +++ b/app/schemas/file.py @@ -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 \ No newline at end of file diff --git a/app/schemas/game.py b/app/schemas/game.py new file mode 100644 index 0000000..4ffc0cc --- /dev/null +++ b/app/schemas/game.py @@ -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 \ No newline at end of file diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..3b5ea14 --- /dev/null +++ b/app/services/__init__.py @@ -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"] \ No newline at end of file diff --git a/app/services/base_service.py b/app/services/base_service.py new file mode 100644 index 0000000..8edeb58 --- /dev/null +++ b/app/services/base_service.py @@ -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) \ No newline at end of file diff --git a/app/services/card_service.py b/app/services/card_service.py new file mode 100644 index 0000000..874b73e --- /dev/null +++ b/app/services/card_service.py @@ -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() \ No newline at end of file diff --git a/app/services/external_api/base_external_service.py b/app/services/external_api/base_external_service.py new file mode 100644 index 0000000..572a1ff --- /dev/null +++ b/app/services/external_api/base_external_service.py @@ -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() \ No newline at end of file diff --git a/app/services/external_api/tcgplayer/base_tcgplayer_service.py b/app/services/external_api/tcgplayer/base_tcgplayer_service.py new file mode 100644 index 0000000..8986429 --- /dev/null +++ b/app/services/external_api/tcgplayer/base_tcgplayer_service.py @@ -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() \ No newline at end of file diff --git a/app/services/external_api/tcgplayer/tcgplayer_credentials.py b/app/services/external_api/tcgplayer/tcgplayer_credentials.py new file mode 100644 index 0000000..6136853 --- /dev/null +++ b/app/services/external_api/tcgplayer/tcgplayer_credentials.py @@ -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() \ No newline at end of file diff --git a/app/services/external_api/tcgplayer/tcgplayer_inventory_service.py b/app/services/external_api/tcgplayer/tcgplayer_inventory_service.py new file mode 100644 index 0000000..64e4bc4 --- /dev/null +++ b/app/services/external_api/tcgplayer/tcgplayer_inventory_service.py @@ -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 diff --git a/app/services/external_api/tcgplayer/tcgplayer_order_service.py b/app/services/external_api/tcgplayer/tcgplayer_order_service.py new file mode 100644 index 0000000..e018c97 --- /dev/null +++ b/app/services/external_api/tcgplayer/tcgplayer_order_service.py @@ -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 diff --git a/app/services/external_api/tcgplayer/tcgplayer_pricing_service.py b/app/services/external_api/tcgplayer/tcgplayer_pricing_service.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/scheduler/base_scheduler.py b/app/services/scheduler/base_scheduler.py new file mode 100644 index 0000000..40f05d3 --- /dev/null +++ b/app/services/scheduler/base_scheduler.py @@ -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") \ No newline at end of file diff --git a/app/services/service_registry.py b/app/services/service_registry.py new file mode 100644 index 0000000..ec3d95d --- /dev/null +++ b/app/services/service_registry.py @@ -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() \ No newline at end of file