so much stuff lol
This commit is contained in:
@ -34,6 +34,8 @@ def transaction(db: Session):
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
# Reset the session state
|
||||
db.expire_all()
|
||||
raise e
|
||||
|
||||
def init_db():
|
||||
|
83
app/main.py
83
app/main.py
@ -1,22 +1,77 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
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
|
||||
import os
|
||||
from app.routes import routes
|
||||
from app.db.database import init_db, SessionLocal
|
||||
from app.services.scheduler.scheduler_service import SchedulerService
|
||||
from app.services.data_initialization import DataInitializationService
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
handlers=[logging.StreamHandler(), logging.FileHandler("app.log")],)
|
||||
# Configure logging
|
||||
log_file = "app.log"
|
||||
if os.path.exists(log_file):
|
||||
os.remove(log_file) # Remove existing log file to start fresh
|
||||
|
||||
# Create a formatter
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
|
||||
|
||||
# Create handlers
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
file_handler = logging.FileHandler(log_file, mode='w', encoding='utf-8')
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# Configure root logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.INFO)
|
||||
root_logger.addHandler(console_handler)
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
# Get logger for this module
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info("Application starting up...")
|
||||
|
||||
# Initialize scheduler service
|
||||
scheduler_service = SchedulerService()
|
||||
data_init_service = DataInitializationService()
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
init_db()
|
||||
logger.info("Database initialized successfully")
|
||||
|
||||
# Initialize TCGPlayer data
|
||||
db = SessionLocal()
|
||||
try:
|
||||
await data_init_service.initialize_data(db, game_ids=[1, 3]) # 1 = Magic, 3 = Pokemon
|
||||
logger.info("TCGPlayer data initialized successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize TCGPlayer data: {str(e)}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Start the scheduler
|
||||
await scheduler_service.start_scheduled_tasks()
|
||||
await scheduler_service.process_tcgplayer_export(export_type="live", use_cache=True)
|
||||
logger.info("Scheduler started successfully")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
await scheduler_service.scheduler.shutdown()
|
||||
await data_init_service.close()
|
||||
logger.info("Scheduler shut down")
|
||||
logger.info("Database connection closed")
|
||||
|
||||
app = FastAPI(
|
||||
title="CCR Cards Management API",
|
||||
description="API for managing CCR Cards Inventory, Orders, and more.",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
@ -29,17 +84,5 @@ app.add_middleware(
|
||||
|
||||
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)
|
||||
uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
@ -12,16 +12,13 @@ class Box(Base):
|
||||
set_code = Column(String)
|
||||
sku = Column(Integer)
|
||||
name = Column(String)
|
||||
game_id = Column(Integer, ForeignKey("games.id"))
|
||||
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"
|
||||
|
||||
@ -31,7 +28,3 @@ class OpenBox(Base):
|
||||
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")
|
||||
|
@ -1,4 +1,3 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime
|
||||
@ -13,11 +12,26 @@ class Card(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True)
|
||||
rarity = Column(String)
|
||||
set_name = Column(String)
|
||||
set_name = Column(String, index=True)
|
||||
price = Column(Float)
|
||||
quantity = Column(Integer, default=0)
|
||||
|
||||
# TCGPlayer specific fields
|
||||
tcgplayer_sku = Column(String, unique=True, index=True)
|
||||
product_line = Column(String)
|
||||
product_name = Column(String)
|
||||
title = Column(String)
|
||||
number = Column(String)
|
||||
condition = Column(String)
|
||||
tcg_market_price = Column(Float)
|
||||
tcg_direct_low = Column(Float)
|
||||
tcg_low_price_with_shipping = Column(Float)
|
||||
tcg_low_price = Column(Float)
|
||||
total_quantity = Column(Integer)
|
||||
add_to_quantity = Column(Integer)
|
||||
tcg_marketplace_price = Column(Float)
|
||||
photo_url = Column(String)
|
||||
|
||||
# Timestamps
|
||||
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")
|
||||
|
@ -15,7 +15,4 @@ class File(Base):
|
||||
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())
|
||||
|
||||
|
||||
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
@ -1,6 +1,3 @@
|
||||
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
|
||||
@ -15,6 +12,3 @@ class Game(Base):
|
||||
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")
|
||||
|
28
app/models/inventory.py
Normal file
28
app/models/inventory.py
Normal file
@ -0,0 +1,28 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
class Inventory(Base):
|
||||
__tablename__ = "inventory"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
tcgplayer_id = Column(String, unique=True, index=True)
|
||||
product_line = Column(String)
|
||||
set_name = Column(String)
|
||||
product_name = Column(String)
|
||||
title = Column(String)
|
||||
number = Column(String)
|
||||
rarity = Column(String)
|
||||
condition = Column(String)
|
||||
tcg_market_price = Column(Float)
|
||||
tcg_direct_low = Column(Float)
|
||||
tcg_low_price_with_shipping = Column(Float)
|
||||
tcg_low_price = Column(Float)
|
||||
total_quantity = Column(Integer)
|
||||
add_to_quantity = Column(Integer)
|
||||
tcg_marketplace_price = Column(Float)
|
||||
photo_url = Column(String)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
25
app/models/order.py
Normal file
25
app/models/order.py
Normal file
@ -0,0 +1,25 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from app.db.database import Base
|
||||
|
||||
class Order(Base):
|
||||
__tablename__ = "orders"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
customer_name = Column(String, index=True)
|
||||
customer_email = Column(String)
|
||||
total_amount = Column(Float)
|
||||
status = Column(String, default="pending") # pending, processing, shipped, delivered, cancelled
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
class OrderCard(Base):
|
||||
__tablename__ = "order_cards"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
order_id = Column(Integer, ForeignKey("orders.id"))
|
||||
card_id = Column(Integer, ForeignKey("cards.id"))
|
||||
quantity = Column(Integer, default=1)
|
||||
price_at_time = Column(Float) # Price of the card when ordered
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
23
app/models/tcgplayer_category.py
Normal file
23
app/models/tcgplayer_category.py
Normal file
@ -0,0 +1,23 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
class TCGPlayerCategory(Base):
|
||||
__tablename__ = "tcgplayer_categories"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
category_id = Column(Integer, unique=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
display_name = Column(String)
|
||||
seo_category_name = Column(String)
|
||||
category_description = Column(String)
|
||||
category_page_title = Column(String)
|
||||
sealed_label = Column(String)
|
||||
non_sealed_label = Column(String)
|
||||
condition_guide_url = Column(String)
|
||||
is_scannable = Column(Boolean, default=False)
|
||||
popularity = Column(Integer, default=0)
|
||||
is_direct = Column(Boolean, default=False)
|
||||
modified_on = Column(DateTime)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
17
app/models/tcgplayer_group.py
Normal file
17
app/models/tcgplayer_group.py
Normal file
@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
class TCGPlayerGroup(Base):
|
||||
__tablename__ = "tcgplayer_groups"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
group_id = Column(Integer, unique=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
abbreviation = Column(String)
|
||||
is_supplemental = Column(Boolean, default=False)
|
||||
published_on = Column(DateTime)
|
||||
modified_on = Column(DateTime)
|
||||
category_id = Column(Integer)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
27
app/models/tcgplayer_product.py
Normal file
27
app/models/tcgplayer_product.py
Normal file
@ -0,0 +1,27 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
|
||||
class TCGPlayerProduct(Base):
|
||||
__tablename__ = "tcgplayer_products"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
product_id = Column(Integer, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
clean_name = Column(String)
|
||||
image_url = Column(String)
|
||||
category_id = Column(Integer)
|
||||
group_id = Column(Integer, ForeignKey("tcgplayer_groups.group_id"))
|
||||
url = Column(String)
|
||||
modified_on = Column(DateTime)
|
||||
image_count = Column(Integer)
|
||||
ext_rarity = Column(String)
|
||||
ext_number = Column(String)
|
||||
low_price = Column(Float)
|
||||
mid_price = Column(Float)
|
||||
high_price = Column(Float)
|
||||
market_price = Column(Float)
|
||||
direct_low_price = Column(Float)
|
||||
sub_type_name = Column(String)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
@ -1,17 +1,25 @@
|
||||
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.models.file import File as FileModel
|
||||
from app.schemas.file import FileCreate, FileUpdate, FileDelete, FileList, FileInDB
|
||||
from app.models.box import Box as BoxModel, OpenBox as OpenBoxModel
|
||||
from app.schemas.box import BoxCreate, BoxUpdate, BoxDelete, BoxList, OpenBoxCreate, OpenBoxUpdate, OpenBoxDelete, OpenBoxList, BoxInDB, OpenBoxInDB
|
||||
from app.models.game import Game as GameModel
|
||||
from app.schemas.game import GameCreate, GameUpdate, GameDelete, GameList, GameInDB
|
||||
from app.models.card import Card as CardModel
|
||||
from app.schemas.card import CardCreate, CardUpdate, CardDelete, CardList, CardInDB
|
||||
from app.services import CardService, OrderService
|
||||
from app.services.file_processing_service import FileProcessingService
|
||||
from app.services.external_api.tcgplayer.tcgplayer_inventory_service import TCGPlayerInventoryService
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
# Initialize services
|
||||
card_service = CardService()
|
||||
order_service = OrderService()
|
||||
file_processing_service = FileProcessingService()
|
||||
tcgplayer_inventory_service = TCGPlayerInventoryService()
|
||||
|
||||
# ============================================================================
|
||||
# Health Check & Root Endpoints
|
||||
@ -37,7 +45,7 @@ async def get_cards(
|
||||
):
|
||||
skip = (page - 1) * limit
|
||||
cards = card_service.get_all(db, skip=skip, limit=limit)
|
||||
total = db.query(Card).count()
|
||||
total = db.query(CardModel).count()
|
||||
return {
|
||||
"cards": cards,
|
||||
"total": total,
|
||||
@ -45,7 +53,7 @@ async def get_cards(
|
||||
"limit": limit
|
||||
}
|
||||
|
||||
@router.post("/cards", response_model=Card)
|
||||
@router.post("/cards", response_model=CardInDB)
|
||||
async def create_card(card: CardCreate, db: Session = Depends(get_db)):
|
||||
try:
|
||||
created_card = card_service.create(db, card.dict())
|
||||
@ -53,7 +61,7 @@ async def create_card(card: CardCreate, db: Session = Depends(get_db)):
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@router.put("/cards/{card_id}", response_model=Card)
|
||||
@router.put("/cards/{card_id}", response_model=CardInDB)
|
||||
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:
|
||||
@ -105,11 +113,11 @@ async def get_orders(
|
||||
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)
|
||||
@router.post("/files", response_model=FileInDB)
|
||||
async def create_file(file: FileCreate):
|
||||
return {"message": "File created successfully"}
|
||||
|
||||
@router.put("/files/{file_id}", response_model=File)
|
||||
@router.put("/files/{file_id}", response_model=FileInDB)
|
||||
async def update_file(file_id: int, file: FileUpdate):
|
||||
return {"message": "File updated successfully"}
|
||||
|
||||
@ -124,11 +132,11 @@ async def delete_file(file_id: int):
|
||||
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)
|
||||
@router.post("/boxes", response_model=BoxInDB)
|
||||
async def create_box(box: BoxCreate):
|
||||
return {"message": "Box created successfully"}
|
||||
|
||||
@router.put("/boxes/{box_id}", response_model=Box)
|
||||
@router.put("/boxes/{box_id}", response_model=BoxInDB)
|
||||
async def update_box(box_id: int, box: BoxUpdate):
|
||||
return {"message": "Box updated successfully"}
|
||||
|
||||
@ -143,11 +151,11 @@ async def delete_box(box_id: int):
|
||||
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)
|
||||
@router.post("/open_boxes", response_model=OpenBoxInDB)
|
||||
async def create_open_box(open_box: OpenBoxCreate):
|
||||
return {"message": "Open box created successfully"}
|
||||
|
||||
@router.put("/open_boxes/{open_box_id}", response_model=OpenBox)
|
||||
@router.put("/open_boxes/{open_box_id}", response_model=OpenBoxInDB)
|
||||
async def update_open_box(open_box_id: int, open_box: OpenBoxUpdate):
|
||||
return {"message": "Open box updated successfully"}
|
||||
|
||||
@ -162,11 +170,11 @@ async def delete_open_box(open_box_id: int):
|
||||
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)
|
||||
@router.post("/games", response_model=GameInDB)
|
||||
async def create_game(game: GameCreate):
|
||||
return {"message": "Game created successfully"}
|
||||
|
||||
@router.put("/games/{game_id}", response_model=Game)
|
||||
@router.put("/games/{game_id}", response_model=GameInDB)
|
||||
async def update_game(game_id: int, game: GameUpdate):
|
||||
return {"message": "Game updated successfully"}
|
||||
|
||||
@ -174,3 +182,26 @@ async def update_game(game_id: int, game: GameUpdate):
|
||||
async def delete_game(game_id: int):
|
||||
return {"message": "Game deleted successfully"}
|
||||
|
||||
@router.post("/tcgplayer/process-export")
|
||||
async def process_tcgplayer_export(export_type: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Download and process a TCGPlayer export file.
|
||||
|
||||
Args:
|
||||
export_type: Type of export to process (staged, live, or pricing)
|
||||
db: Database session
|
||||
"""
|
||||
try:
|
||||
# Download the file
|
||||
file_bytes = await tcgplayer_inventory_service.get_tcgplayer_export(export_type)
|
||||
|
||||
# Process the file and load into database
|
||||
stats = await file_processing_service.process_tcgplayer_export(db, file_bytes)
|
||||
|
||||
return {
|
||||
"message": "Export processed successfully",
|
||||
"stats": stats
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
@ -1,50 +1,41 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional, List
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
# 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
|
||||
description: Optional[str] = None
|
||||
game_id: int
|
||||
set_id: Optional[int] = None
|
||||
price: Optional[float] = None
|
||||
quantity: Optional[int] = 0
|
||||
status: Optional[str] = "available" # available, sold, reserved
|
||||
|
||||
# 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
|
||||
class BoxUpdate(BoxBase):
|
||||
pass
|
||||
|
||||
# Schema for reading a box
|
||||
class Box(BoxBase):
|
||||
class BoxInDB(BoxBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
cards: List["Card"] = []
|
||||
open_boxes: List["OpenBox"] = []
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Schema for deleting a box
|
||||
class BoxDelete(BaseModel):
|
||||
id: int
|
||||
message: str
|
||||
|
||||
# Schema for listing boxes
|
||||
class BoxList(BaseModel):
|
||||
boxes: List[Box]
|
||||
boxes: List[BoxInDB]
|
||||
total: int
|
||||
page: int
|
||||
limit: int
|
||||
@ -52,30 +43,30 @@ class BoxList(BaseModel):
|
||||
# OpenBox schemas
|
||||
class OpenBoxBase(BaseModel):
|
||||
box_id: int
|
||||
number_of_cards: int
|
||||
date_opened: datetime
|
||||
opened_at: Optional[datetime] = None
|
||||
opened_by: Optional[str] = None
|
||||
contents: Optional[List[dict]] = None
|
||||
status: Optional[str] = "pending" # pending, opened, verified, listed
|
||||
|
||||
class OpenBoxCreate(OpenBoxBase):
|
||||
pass
|
||||
|
||||
class OpenBoxUpdate(BaseModel):
|
||||
number_of_cards: Optional[int] = None
|
||||
date_opened: Optional[datetime] = None
|
||||
class OpenBoxUpdate(OpenBoxBase):
|
||||
pass
|
||||
|
||||
class OpenBox(OpenBoxBase):
|
||||
class OpenBoxInDB(OpenBoxBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
box: Optional[Box] = None
|
||||
cards: List["Card"] = []
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class OpenBoxDelete(BaseModel):
|
||||
id: int
|
||||
message: str
|
||||
|
||||
class OpenBoxList(BaseModel):
|
||||
open_boxes: List[OpenBox]
|
||||
open_boxes: List[OpenBoxInDB]
|
||||
total: int
|
||||
page: int
|
||||
limit: int
|
@ -1,39 +1,55 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional, List
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Base schema with common attributes
|
||||
class CardBase(BaseModel):
|
||||
name: str
|
||||
rarity: str
|
||||
set_name: str
|
||||
price: float
|
||||
quantity: int = 0
|
||||
rarity: Optional[str] = None
|
||||
set_name: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
quantity: Optional[int] = 0
|
||||
|
||||
# TCGPlayer specific fields
|
||||
tcgplayer_sku: Optional[str] = None
|
||||
product_line: Optional[str] = None
|
||||
product_name: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
number: Optional[str] = None
|
||||
condition: Optional[str] = None
|
||||
tcg_market_price: Optional[float] = None
|
||||
tcg_direct_low: Optional[float] = None
|
||||
tcg_low_price_with_shipping: Optional[float] = None
|
||||
tcg_low_price: Optional[float] = None
|
||||
total_quantity: Optional[int] = None
|
||||
add_to_quantity: Optional[int] = None
|
||||
tcg_marketplace_price: Optional[float] = None
|
||||
photo_url: Optional[str] = None
|
||||
|
||||
# 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
|
||||
class CardUpdate(CardBase):
|
||||
pass
|
||||
|
||||
# Schema for reading a card (includes id and relationships)
|
||||
class Card(CardBase):
|
||||
class CardInDB(CardBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
orders: List["Order"] = []
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Schema for listing cards
|
||||
class CardList(BaseModel):
|
||||
cards: List[Card]
|
||||
cards: List[CardInDB]
|
||||
total: int
|
||||
page: int
|
||||
limit: int
|
||||
limit: int
|
||||
|
||||
# Schema for deleting a card
|
||||
class CardDelete(BaseModel):
|
||||
message: str
|
@ -1,38 +1,40 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional, List
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Base schema with common attributes
|
||||
class FileBase(BaseModel):
|
||||
name: str
|
||||
type: str
|
||||
path: str
|
||||
type: Optional[str] = None
|
||||
size: Optional[int] = None
|
||||
content_type: Optional[str] = None
|
||||
metadata: Optional[dict] = None
|
||||
|
||||
# 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
|
||||
class FileUpdate(FileBase):
|
||||
pass
|
||||
|
||||
# Schema for reading a file
|
||||
class File(FileBase):
|
||||
class FileInDB(FileBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Schema for deleting a file
|
||||
class FileDelete(BaseModel):
|
||||
id: int
|
||||
message: str
|
||||
|
||||
# Schema for listing files
|
||||
class FileList(BaseModel):
|
||||
files: List[File]
|
||||
files: List[FileInDB]
|
||||
total: int
|
||||
page: int
|
||||
limit: int
|
@ -1,39 +1,41 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional, List
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Base schema with common attributes
|
||||
class GameBase(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
image_url: str
|
||||
publisher: Optional[str] = None
|
||||
release_date: Optional[datetime] = None
|
||||
description: Optional[str] = None
|
||||
website: Optional[str] = None
|
||||
logo_url: Optional[str] = None
|
||||
status: Optional[str] = "active" # active, inactive, discontinued
|
||||
|
||||
# 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
|
||||
class GameUpdate(GameBase):
|
||||
pass
|
||||
|
||||
# Schema for reading a game
|
||||
class Game(GameBase):
|
||||
class GameInDB(GameBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
boxes: List["Box"] = []
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
# Schema for deleting a game
|
||||
class GameDelete(BaseModel):
|
||||
id: int
|
||||
message: str
|
||||
|
||||
# Schema for listing games
|
||||
class GameList(BaseModel):
|
||||
games: List[Game]
|
||||
games: List[GameInDB]
|
||||
total: int
|
||||
page: int
|
||||
limit: int
|
@ -1,5 +1,15 @@
|
||||
from app.services.base_service import BaseService
|
||||
from app.services.card_service import CardService
|
||||
from app.services.order_service import OrderService
|
||||
from app.services.file_processing_service import FileProcessingService
|
||||
from app.services.inventory_service import InventoryService
|
||||
from app.services.service_registry import ServiceRegistry
|
||||
|
||||
__all__ = ["BaseService", "CardService", "ServiceRegistry"]
|
||||
__all__ = [
|
||||
'BaseService',
|
||||
'CardService',
|
||||
'OrderService',
|
||||
'FileProcessingService',
|
||||
'InventoryService',
|
||||
'ServiceRegistry'
|
||||
]
|
@ -1,17 +1,126 @@
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Dict
|
||||
from sqlalchemy.orm import Session
|
||||
from app.services.base_service import BaseService
|
||||
from app.models.card import Card
|
||||
from app.services.base_service import BaseService
|
||||
from app.schemas.card import CardCreate, CardUpdate
|
||||
|
||||
class CardService(BaseService[Card]):
|
||||
def __init__(self):
|
||||
super().__init__(Card)
|
||||
|
||||
def create(self, db: Session, obj_in: Dict) -> Card:
|
||||
"""
|
||||
Create a new card in the database.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
obj_in: Dictionary containing card data
|
||||
|
||||
Returns:
|
||||
Card: The created card object
|
||||
"""
|
||||
return super().create(db, obj_in)
|
||||
|
||||
def update(self, db: Session, db_obj: Card, obj_in: Dict) -> Card:
|
||||
"""
|
||||
Update an existing card in the database.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_obj: The card object to update
|
||||
obj_in: Dictionary containing updated card data
|
||||
|
||||
Returns:
|
||||
Card: The updated card object
|
||||
"""
|
||||
return super().update(db, db_obj, obj_in)
|
||||
|
||||
def get_by_name(self, db: Session, name: str) -> Optional[Card]:
|
||||
"""
|
||||
Get a card by its name.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
name: The name of the card to find
|
||||
|
||||
Returns:
|
||||
Optional[Card]: The card if found, None otherwise
|
||||
"""
|
||||
return db.query(self.model).filter(self.model.name == name).first()
|
||||
|
||||
def get_by_set(self, db: Session, set_name: str, skip: int = 0, limit: int = 100) -> List[Card]:
|
||||
"""
|
||||
Get all cards from a specific set.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
set_name: The name of the set to filter by
|
||||
skip: Number of records to skip (for pagination)
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
List[Card]: List of cards from the specified set
|
||||
"""
|
||||
return db.query(self.model).filter(self.model.set_name == set_name).offset(skip).limit(limit).all()
|
||||
|
||||
def get_by_rarity(self, db: Session, rarity: str, skip: int = 0, limit: int = 100) -> List[Card]:
|
||||
"""
|
||||
Get all cards of a specific rarity.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
rarity: The rarity to filter by
|
||||
skip: Number of records to skip (for pagination)
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
List[Card]: List of cards with the specified rarity
|
||||
"""
|
||||
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()
|
||||
def update_quantity(self, db: Session, card_id: int, quantity_change: int) -> Card:
|
||||
"""
|
||||
Update the quantity of a card.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
card_id: The ID of the card to update
|
||||
quantity_change: The amount to change the quantity by (can be positive or negative)
|
||||
|
||||
Returns:
|
||||
Card: The updated card object
|
||||
|
||||
Raises:
|
||||
ValueError: If the card is not found or if the resulting quantity would be negative
|
||||
"""
|
||||
card = self.get(db, card_id)
|
||||
if not card:
|
||||
raise ValueError(f"Card with ID {card_id} not found")
|
||||
|
||||
new_quantity = card.quantity + quantity_change
|
||||
if new_quantity < 0:
|
||||
raise ValueError(f"Cannot reduce quantity below 0. Current quantity: {card.quantity}, attempted change: {quantity_change}")
|
||||
|
||||
card.quantity = new_quantity
|
||||
db.add(card)
|
||||
db.commit()
|
||||
db.refresh(card)
|
||||
return card
|
||||
|
||||
def search(self, db: Session, query: str, skip: int = 0, limit: int = 100) -> List[Card]:
|
||||
"""
|
||||
Search for cards by name or set name.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
query: The search query
|
||||
skip: Number of records to skip (for pagination)
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
List[Card]: List of cards matching the search query
|
||||
"""
|
||||
return db.query(self.model).filter(
|
||||
(self.model.name.ilike(f"%{query}%")) |
|
||||
(self.model.set_name.ilike(f"%{query}%"))
|
||||
).offset(skip).limit(limit).all()
|
201
app/services/data_initialization.py
Normal file
201
app/services/data_initialization.py
Normal file
@ -0,0 +1,201 @@
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from sqlalchemy.orm import Session
|
||||
from app.services.external_api.tcgcsv.tcgcsv_service import TCGCSVService
|
||||
from app.models.tcgplayer_group import TCGPlayerGroup
|
||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
||||
from app.models.tcgplayer_category import TCGPlayerCategory
|
||||
|
||||
class DataInitializationService:
|
||||
def __init__(self, cache_dir: str = "app/data/cache/tcgcsv"):
|
||||
self.cache_dir = cache_dir
|
||||
self.tcgcsv_service = TCGCSVService()
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
def _get_cache_path(self, filename: str) -> str:
|
||||
return os.path.join(self.cache_dir, filename)
|
||||
|
||||
async def _cache_categories(self, categories_data: dict):
|
||||
"""Cache categories data to a JSON file"""
|
||||
cache_path = self._get_cache_path("categories.json")
|
||||
with open(cache_path, 'w') as f:
|
||||
json.dump(categories_data, f, indent=2)
|
||||
|
||||
async def _cache_groups(self, game_ids: List[int], groups_data: dict):
|
||||
for game_id in game_ids:
|
||||
cache_path = self._get_cache_path(f"groups_{game_id}.json")
|
||||
with open(cache_path, 'w') as f:
|
||||
json.dump(groups_data, f, default=str)
|
||||
|
||||
async def _cache_products(self, game_ids: List[int], group_id: int, products_data: list):
|
||||
for game_id in game_ids:
|
||||
cache_path = self._get_cache_path(f"products_{game_id}_{group_id}.json")
|
||||
with open(cache_path, 'w') as f:
|
||||
json.dump(products_data, f, default=str)
|
||||
|
||||
async def _load_cached_categories(self) -> Optional[dict]:
|
||||
cache_path = self._get_cache_path("categories.json")
|
||||
if os.path.exists(cache_path):
|
||||
with open(cache_path, 'r') as f:
|
||||
return json.load(f)
|
||||
return None
|
||||
|
||||
async def _load_cached_groups(self, game_ids: List[int]) -> Optional[dict]:
|
||||
# Try to load cached data for any of the game IDs
|
||||
for game_id in game_ids:
|
||||
cache_path = self._get_cache_path(f"groups_{game_id}.json")
|
||||
if os.path.exists(cache_path):
|
||||
with open(cache_path, 'r') as f:
|
||||
return json.load(f)
|
||||
return None
|
||||
|
||||
async def _load_cached_products(self, game_ids: List[int], group_id: int) -> Optional[list]:
|
||||
# Try to load cached data for any of the game IDs
|
||||
for game_id in game_ids:
|
||||
cache_path = self._get_cache_path(f"products_{game_id}_{group_id}.json")
|
||||
if os.path.exists(cache_path):
|
||||
with open(cache_path, 'r') as f:
|
||||
return json.load(f)
|
||||
return None
|
||||
|
||||
async def initialize_data(self, db: Session, game_ids: List[int], use_cache: bool = True) -> None:
|
||||
"""Initialize TCGPlayer data, using cache if available and requested"""
|
||||
print("Initializing TCGPlayer data...")
|
||||
|
||||
# Handle categories
|
||||
categories_data = None
|
||||
if use_cache:
|
||||
categories_data = await self._load_cached_categories()
|
||||
|
||||
if not categories_data:
|
||||
print("Fetching categories from API...")
|
||||
categories_data = await self.tcgcsv_service.get_categories()
|
||||
if use_cache:
|
||||
await self._cache_categories(categories_data)
|
||||
|
||||
if not categories_data.get("success"):
|
||||
raise Exception(f"Failed to fetch categories: {categories_data.get('errors')}")
|
||||
|
||||
# Sync categories to database
|
||||
categories = categories_data.get("results", [])
|
||||
synced_categories = []
|
||||
for category_data in categories:
|
||||
existing_category = db.query(TCGPlayerCategory).filter(TCGPlayerCategory.category_id == category_data["categoryId"]).first()
|
||||
if existing_category:
|
||||
synced_categories.append(existing_category)
|
||||
else:
|
||||
new_category = TCGPlayerCategory(
|
||||
category_id=category_data["categoryId"],
|
||||
name=category_data["name"],
|
||||
display_name=category_data.get("displayName"),
|
||||
seo_category_name=category_data.get("seoCategoryName"),
|
||||
category_description=category_data.get("categoryDescription"),
|
||||
category_page_title=category_data.get("categoryPageTitle"),
|
||||
sealed_label=category_data.get("sealedLabel"),
|
||||
non_sealed_label=category_data.get("nonSealedLabel"),
|
||||
condition_guide_url=category_data.get("conditionGuideUrl"),
|
||||
is_scannable=category_data.get("isScannable", False),
|
||||
popularity=category_data.get("popularity", 0),
|
||||
is_direct=category_data.get("isDirect", False),
|
||||
modified_on=datetime.fromisoformat(category_data["modifiedOn"].replace("Z", "+00:00")) if category_data.get("modifiedOn") else None
|
||||
)
|
||||
db.add(new_category)
|
||||
synced_categories.append(new_category)
|
||||
db.commit()
|
||||
print(f"Synced {len(synced_categories)} categories")
|
||||
|
||||
# Process each game ID separately
|
||||
for game_id in game_ids:
|
||||
print(f"\nProcessing game ID: {game_id}")
|
||||
|
||||
# Handle groups for this game ID
|
||||
groups_data = None
|
||||
if use_cache:
|
||||
groups_data = await self._load_cached_groups([game_id])
|
||||
|
||||
if not groups_data:
|
||||
print(f"Fetching groups for game ID {game_id} from API...")
|
||||
groups_data = await self.tcgcsv_service.get_groups([game_id])
|
||||
if use_cache:
|
||||
await self._cache_groups([game_id], groups_data)
|
||||
|
||||
if not groups_data.get("success"):
|
||||
raise Exception(f"Failed to fetch groups for game ID {game_id}: {groups_data.get('errors')}")
|
||||
|
||||
# Sync groups to database
|
||||
groups = groups_data.get("results", [])
|
||||
synced_groups = []
|
||||
for group_data in groups:
|
||||
existing_group = db.query(TCGPlayerGroup).filter(TCGPlayerGroup.group_id == group_data["groupId"]).first()
|
||||
if existing_group:
|
||||
synced_groups.append(existing_group)
|
||||
else:
|
||||
new_group = TCGPlayerGroup(
|
||||
group_id=group_data["groupId"],
|
||||
name=group_data["name"],
|
||||
abbreviation=group_data.get("abbreviation"),
|
||||
is_supplemental=group_data.get("isSupplemental", False),
|
||||
published_on=datetime.fromisoformat(group_data["publishedOn"].replace("Z", "+00:00")) if group_data.get("publishedOn") else None,
|
||||
modified_on=datetime.fromisoformat(group_data["modifiedOn"].replace("Z", "+00:00")) if group_data.get("modifiedOn") else None,
|
||||
category_id=group_data.get("categoryId")
|
||||
)
|
||||
db.add(new_group)
|
||||
synced_groups.append(new_group)
|
||||
db.commit()
|
||||
print(f"Synced {len(synced_groups)} groups for game ID {game_id}")
|
||||
|
||||
# Handle products for each group in this game ID
|
||||
for group in synced_groups:
|
||||
products_data = None
|
||||
if use_cache:
|
||||
products_data = await self._load_cached_products([game_id], group.group_id)
|
||||
|
||||
if not products_data:
|
||||
print(f"Fetching products for group {group.name} (game ID {game_id}) from API...")
|
||||
products_data = await self.tcgcsv_service.get_products_and_prices([game_id], group.group_id)
|
||||
if use_cache:
|
||||
await self._cache_products([game_id], group.group_id, products_data)
|
||||
|
||||
# Sync products to database
|
||||
synced_products = []
|
||||
for product_data in products_data:
|
||||
existing_product = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.product_id == int(product_data["productId"])).first()
|
||||
if existing_product:
|
||||
synced_products.append(existing_product)
|
||||
else:
|
||||
new_product = TCGPlayerProduct(
|
||||
product_id=int(product_data["productId"]),
|
||||
name=product_data["name"],
|
||||
clean_name=product_data.get("cleanName"),
|
||||
image_url=product_data.get("imageUrl"),
|
||||
category_id=int(product_data["categoryId"]),
|
||||
group_id=int(product_data["groupId"]),
|
||||
url=product_data.get("url"),
|
||||
modified_on=datetime.fromisoformat(product_data["modifiedOn"].replace("Z", "+00:00")) if product_data.get("modifiedOn") else None,
|
||||
image_count=int(product_data.get("imageCount", 0)),
|
||||
ext_rarity=product_data.get("extRarity"),
|
||||
ext_number=product_data.get("extNumber"),
|
||||
low_price=float(product_data.get("lowPrice")) if product_data.get("lowPrice") else None,
|
||||
mid_price=float(product_data.get("midPrice")) if product_data.get("midPrice") else None,
|
||||
high_price=float(product_data.get("highPrice")) if product_data.get("highPrice") else None,
|
||||
market_price=float(product_data.get("marketPrice")) if product_data.get("marketPrice") else None,
|
||||
direct_low_price=float(product_data.get("directLowPrice")) if product_data.get("directLowPrice") else None,
|
||||
sub_type_name=product_data.get("subTypeName")
|
||||
)
|
||||
db.add(new_product)
|
||||
synced_products.append(new_product)
|
||||
db.commit()
|
||||
print(f"Synced {len(synced_products)} products for group {group.name} (game ID {game_id})")
|
||||
|
||||
async def clear_cache(self) -> None:
|
||||
"""Clear all cached data"""
|
||||
for filename in os.listdir(self.cache_dir):
|
||||
file_path = os.path.join(self.cache_dir, filename)
|
||||
if os.path.isfile(file_path):
|
||||
os.unlink(file_path)
|
||||
print("Cache cleared")
|
||||
|
||||
async def close(self):
|
||||
await self.tcgcsv_service.close()
|
@ -1,7 +1,8 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, Optional, Union
|
||||
import aiohttp
|
||||
import logging
|
||||
from app.services.service_registry import ServiceRegistry
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -24,8 +25,9 @@ class BaseExternalService:
|
||||
endpoint: str,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
content_type: str = "application/json"
|
||||
) -> Union[Dict[str, Any], str]:
|
||||
session = await self._get_session()
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
@ -36,9 +38,30 @@ class BaseExternalService:
|
||||
try:
|
||||
async with session.request(method, url, params=params, headers=headers, json=data) as response:
|
||||
response.raise_for_status()
|
||||
return await response.json()
|
||||
|
||||
# Get the actual content type from the response
|
||||
response_content_type = response.headers.get('content-type', '').lower()
|
||||
logger.info(f"Making request to {url}")
|
||||
|
||||
# Get the raw response text first
|
||||
raw_response = await response.text()
|
||||
|
||||
# Only try to parse as JSON if the content type indicates JSON
|
||||
if 'application/json' in response_content_type or 'text/json' in response_content_type:
|
||||
try:
|
||||
# First try to parse the response directly
|
||||
return await response.json()
|
||||
except Exception as e:
|
||||
try:
|
||||
# If that fails, try parsing the raw text as JSON (in case it's double-encoded)
|
||||
return json.loads(raw_response)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse JSON response: {e}")
|
||||
return raw_response
|
||||
return raw_response
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"API request failed: {str(e)}")
|
||||
logger.error(f"Request failed: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during API request: {str(e)}")
|
||||
|
205
app/services/external_api/tcgcsv/tcgcsv_service.py
Normal file
205
app/services/external_api/tcgcsv/tcgcsv_service.py
Normal file
@ -0,0 +1,205 @@
|
||||
from typing import List, Dict, Any
|
||||
from datetime import datetime
|
||||
import csv
|
||||
import io
|
||||
from app.services.external_api.base_external_service import BaseExternalService
|
||||
from app.models.tcgplayer_group import TCGPlayerGroup
|
||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
||||
from app.models.tcgplayer_category import TCGPlayerCategory
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
class TCGCSVService(BaseExternalService):
|
||||
def __init__(self):
|
||||
super().__init__(base_url="https://tcgcsv.com/tcgplayer/")
|
||||
|
||||
async def get_groups(self, game_ids: List[int]) -> Dict[str, Any]:
|
||||
"""Fetch groups for specific game IDs from TCGCSV API"""
|
||||
game_ids_str = ",".join(map(str, game_ids))
|
||||
endpoint = f"{game_ids_str}/groups"
|
||||
return await self._make_request("GET", endpoint)
|
||||
|
||||
async def get_products_and_prices(self, game_ids: List[int], group_id: int) -> List[Dict[str, Any]]:
|
||||
"""Fetch products and prices for a specific group from TCGCSV API"""
|
||||
game_ids_str = ",".join(map(str, game_ids))
|
||||
endpoint = f"{game_ids_str}/{group_id}/ProductsAndPrices.csv"
|
||||
response = await self._make_request("GET", endpoint, headers={"Accept": "text/csv"})
|
||||
|
||||
# Parse CSV response
|
||||
csv_data = io.StringIO(response)
|
||||
reader = csv.DictReader(csv_data)
|
||||
return list(reader)
|
||||
|
||||
async def get_categories(self) -> Dict[str, Any]:
|
||||
"""Fetch all categories from TCGCSV API"""
|
||||
endpoint = "categories"
|
||||
return await self._make_request("GET", endpoint)
|
||||
|
||||
async def sync_groups_to_db(self, db: Session, game_ids: List[int]) -> List[TCGPlayerGroup]:
|
||||
"""Fetch groups from API and sync them to the database"""
|
||||
response = await self.get_groups(game_ids)
|
||||
|
||||
if not response.get("success"):
|
||||
raise Exception(f"Failed to fetch groups: {response.get('errors')}")
|
||||
|
||||
groups = response.get("results", [])
|
||||
synced_groups = []
|
||||
|
||||
for group_data in groups:
|
||||
# Convert string dates to datetime objects
|
||||
published_on = datetime.fromisoformat(group_data["publishedOn"].replace("Z", "+00:00")) if group_data.get("publishedOn") else None
|
||||
modified_on = datetime.fromisoformat(group_data["modifiedOn"].replace("Z", "+00:00")) if group_data.get("modifiedOn") else None
|
||||
|
||||
# Check if group already exists
|
||||
existing_group = db.query(TCGPlayerGroup).filter(TCGPlayerGroup.group_id == group_data["groupId"]).first()
|
||||
|
||||
if existing_group:
|
||||
# Update existing group
|
||||
for key, value in {
|
||||
"name": group_data["name"],
|
||||
"abbreviation": group_data.get("abbreviation"),
|
||||
"is_supplemental": group_data.get("isSupplemental", False),
|
||||
"published_on": published_on,
|
||||
"modified_on": modified_on,
|
||||
"category_id": group_data.get("categoryId")
|
||||
}.items():
|
||||
setattr(existing_group, key, value)
|
||||
synced_groups.append(existing_group)
|
||||
else:
|
||||
# Create new group
|
||||
new_group = TCGPlayerGroup(
|
||||
group_id=group_data["groupId"],
|
||||
name=group_data["name"],
|
||||
abbreviation=group_data.get("abbreviation"),
|
||||
is_supplemental=group_data.get("isSupplemental", False),
|
||||
published_on=published_on,
|
||||
modified_on=modified_on,
|
||||
category_id=group_data.get("categoryId")
|
||||
)
|
||||
db.add(new_group)
|
||||
synced_groups.append(new_group)
|
||||
|
||||
db.commit()
|
||||
return synced_groups
|
||||
|
||||
async def sync_products_to_db(self, db: Session, game_id: int, group_id: int) -> List[TCGPlayerProduct]:
|
||||
"""Fetch products and prices for a group and sync them to the database"""
|
||||
products_data = await self.get_products_and_prices(game_id, group_id)
|
||||
synced_products = []
|
||||
|
||||
for product_data in products_data:
|
||||
# Convert string dates to datetime objects
|
||||
modified_on = datetime.fromisoformat(product_data["modifiedOn"].replace("Z", "+00:00")) if product_data.get("modifiedOn") else None
|
||||
|
||||
# Convert price strings to floats, handling empty strings
|
||||
def parse_price(price_str):
|
||||
return float(price_str) if price_str else None
|
||||
|
||||
# Check if product already exists
|
||||
existing_product = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.product_id == int(product_data["productId"])).first()
|
||||
|
||||
if existing_product:
|
||||
# Update existing product
|
||||
for key, value in {
|
||||
"name": product_data["name"],
|
||||
"clean_name": product_data.get("cleanName"),
|
||||
"image_url": product_data.get("imageUrl"),
|
||||
"category_id": int(product_data["categoryId"]),
|
||||
"group_id": int(product_data["groupId"]),
|
||||
"url": product_data.get("url"),
|
||||
"modified_on": modified_on,
|
||||
"image_count": int(product_data.get("imageCount", 0)),
|
||||
"ext_rarity": product_data.get("extRarity"),
|
||||
"ext_number": product_data.get("extNumber"),
|
||||
"low_price": parse_price(product_data.get("lowPrice")),
|
||||
"mid_price": parse_price(product_data.get("midPrice")),
|
||||
"high_price": parse_price(product_data.get("highPrice")),
|
||||
"market_price": parse_price(product_data.get("marketPrice")),
|
||||
"direct_low_price": parse_price(product_data.get("directLowPrice")),
|
||||
"sub_type_name": product_data.get("subTypeName")
|
||||
}.items():
|
||||
setattr(existing_product, key, value)
|
||||
synced_products.append(existing_product)
|
||||
else:
|
||||
# Create new product
|
||||
new_product = TCGPlayerProduct(
|
||||
product_id=int(product_data["productId"]),
|
||||
name=product_data["name"],
|
||||
clean_name=product_data.get("cleanName"),
|
||||
image_url=product_data.get("imageUrl"),
|
||||
category_id=int(product_data["categoryId"]),
|
||||
group_id=int(product_data["groupId"]),
|
||||
url=product_data.get("url"),
|
||||
modified_on=modified_on,
|
||||
image_count=int(product_data.get("imageCount", 0)),
|
||||
ext_rarity=product_data.get("extRarity"),
|
||||
ext_number=product_data.get("extNumber"),
|
||||
low_price=parse_price(product_data.get("lowPrice")),
|
||||
mid_price=parse_price(product_data.get("midPrice")),
|
||||
high_price=parse_price(product_data.get("highPrice")),
|
||||
market_price=parse_price(product_data.get("marketPrice")),
|
||||
direct_low_price=parse_price(product_data.get("directLowPrice")),
|
||||
sub_type_name=product_data.get("subTypeName")
|
||||
)
|
||||
db.add(new_product)
|
||||
synced_products.append(new_product)
|
||||
|
||||
db.commit()
|
||||
return synced_products
|
||||
|
||||
async def sync_categories_to_db(self, db: Session) -> List[TCGPlayerCategory]:
|
||||
"""Fetch categories from API and sync them to the database"""
|
||||
response = await self.get_categories()
|
||||
|
||||
if not response.get("success"):
|
||||
raise Exception(f"Failed to fetch categories: {response.get('errors')}")
|
||||
|
||||
categories = response.get("results", [])
|
||||
synced_categories = []
|
||||
|
||||
for category_data in categories:
|
||||
# Convert string dates to datetime objects
|
||||
modified_on = datetime.fromisoformat(category_data["modifiedOn"].replace("Z", "+00:00")) if category_data.get("modifiedOn") else None
|
||||
|
||||
# Check if category already exists
|
||||
existing_category = db.query(TCGPlayerCategory).filter(TCGPlayerCategory.category_id == category_data["categoryId"]).first()
|
||||
|
||||
if existing_category:
|
||||
# Update existing category
|
||||
for key, value in {
|
||||
"name": category_data["name"],
|
||||
"display_name": category_data.get("displayName"),
|
||||
"seo_category_name": category_data.get("seoCategoryName"),
|
||||
"category_description": category_data.get("categoryDescription"),
|
||||
"category_page_title": category_data.get("categoryPageTitle"),
|
||||
"sealed_label": category_data.get("sealedLabel"),
|
||||
"non_sealed_label": category_data.get("nonSealedLabel"),
|
||||
"condition_guide_url": category_data.get("conditionGuideUrl"),
|
||||
"is_scannable": category_data.get("isScannable", False),
|
||||
"popularity": category_data.get("popularity", 0),
|
||||
"is_direct": category_data.get("isDirect", False),
|
||||
"modified_on": modified_on
|
||||
}.items():
|
||||
setattr(existing_category, key, value)
|
||||
synced_categories.append(existing_category)
|
||||
else:
|
||||
# Create new category
|
||||
new_category = TCGPlayerCategory(
|
||||
category_id=category_data["categoryId"],
|
||||
name=category_data["name"],
|
||||
display_name=category_data.get("displayName"),
|
||||
seo_category_name=category_data.get("seoCategoryName"),
|
||||
category_description=category_data.get("categoryDescription"),
|
||||
category_page_title=category_data.get("categoryPageTitle"),
|
||||
sealed_label=category_data.get("sealedLabel"),
|
||||
non_sealed_label=category_data.get("nonSealedLabel"),
|
||||
condition_guide_url=category_data.get("conditionGuideUrl"),
|
||||
is_scannable=category_data.get("isScannable", False),
|
||||
popularity=category_data.get("popularity", 0),
|
||||
is_direct=category_data.get("isDirect", False),
|
||||
modified_on=modified_on
|
||||
)
|
||||
db.add(new_category)
|
||||
synced_categories.append(new_category)
|
||||
|
||||
db.commit()
|
||||
return synced_categories
|
@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, Optional, Union
|
||||
import aiohttp
|
||||
import logging
|
||||
from app.services.external_api.base_external_service import BaseExternalService
|
||||
@ -7,14 +7,20 @@ from app.services.external_api.tcgplayer.tcgplayer_credentials import TCGPlayerC
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BaseTCGPlayerService(BaseExternalService):
|
||||
STORE_BASE_URL = "https://store.tcgplayer.com"
|
||||
LOGIN_ENDPOINT = "/oauth/login"
|
||||
PRICING_ENDPOINT = "/Admin/Pricing"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
store_base_url="https://store.tcgplayer.com",
|
||||
login_endpoint="/oauth/login",
|
||||
pricing_endpoint="/Admin/Pricing",
|
||||
staged_inventory_endpoint=self.pricing_endpoint + "/DownloadStagedInventoryExportCSV?type=Pricing",
|
||||
live_inventory_endpoint=self.pricing_endpoint + "/DownloadMyExportCSV?type=Pricing"
|
||||
)
|
||||
super().__init__(base_url=self.STORE_BASE_URL)
|
||||
|
||||
# Set up endpoints
|
||||
self.login_endpoint = self.LOGIN_ENDPOINT
|
||||
self.pricing_endpoint = self.PRICING_ENDPOINT
|
||||
self.staged_inventory_endpoint = f"{self.PRICING_ENDPOINT}/DownloadStagedInventoryExportCSV?type=Pricing"
|
||||
self.live_inventory_endpoint = f"{self.PRICING_ENDPOINT}/DownloadMyExportCSV?type=Pricing"
|
||||
self.pricing_export_endpoint = f"{self.PRICING_ENDPOINT}/downloadexportcsv"
|
||||
|
||||
self.credentials = TCGPlayerCredentials()
|
||||
|
||||
def _get_headers(self, method: str) -> Dict[str, str]:
|
||||
@ -53,10 +59,11 @@ class BaseTCGPlayerService(BaseExternalService):
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
auth_required: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
auth_required: bool = True,
|
||||
download_file: bool = False
|
||||
) -> Union[Dict[str, Any], bytes]:
|
||||
session = await self._get_session()
|
||||
url = f"{self.store_base_url}{endpoint}"
|
||||
url = f"{self.base_url}{endpoint}"
|
||||
|
||||
# Get the authentication cookie if required
|
||||
if auth_required:
|
||||
@ -77,6 +84,9 @@ class BaseTCGPlayerService(BaseExternalService):
|
||||
if response.status == 401:
|
||||
raise RuntimeError("TCGPlayer authentication failed. Cookie may be invalid or expired.")
|
||||
response.raise_for_status()
|
||||
|
||||
if download_file:
|
||||
return await response.read()
|
||||
return await response.json()
|
||||
except aiohttp.ClientError as e:
|
||||
logger.error(f"TCGPlayer API request failed: {str(e)}")
|
||||
|
@ -5,14 +5,17 @@ class TCGPlayerInventoryService(BaseTCGPlayerService):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
async def get_inventory(self) -> List[Dict]:
|
||||
"""Get inventory items"""
|
||||
endpoint = "/inventory"
|
||||
response = await self._make_request("GET", endpoint)
|
||||
return response.get("results", [])
|
||||
|
||||
async def update_inventory(self, updates: List[Dict]) -> Dict:
|
||||
"""Update inventory items"""
|
||||
endpoint = "/inventory"
|
||||
response = await self._make_request("PUT", endpoint, data=updates)
|
||||
return response
|
||||
async def get_tcgplayer_export(self, export_type: str):
|
||||
"""
|
||||
Get a TCGPlayer Staged Inventory Export, Live Inventory Export, or Pricing Export
|
||||
"""
|
||||
if export_type == "staged":
|
||||
endpoint = self.staged_inventory_endpoint
|
||||
elif export_type == "live":
|
||||
endpoint = self.live_inventory_endpoint
|
||||
elif export_type == "pricing":
|
||||
endpoint = self.pricing_export_endpoint
|
||||
else:
|
||||
raise ValueError(f"Invalid export type: {export_type}, must be 'staged', 'live', or 'pricing'")
|
||||
file_bytes = await self._make_request("GET", endpoint, download_file=True)
|
||||
return file_bytes
|
@ -1,106 +0,0 @@
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
|
||||
|
||||
class TCGPlayerOrderService(BaseTCGPlayerService):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
async def get_orders(
|
||||
self,
|
||||
status: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
limit: int = 100
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Get a list of orders with optional filtering
|
||||
|
||||
Args:
|
||||
status: Filter by order status (e.g., "Shipped", "Processing")
|
||||
start_date: Filter orders after this date
|
||||
end_date: Filter orders before this date
|
||||
limit: Maximum number of orders to return
|
||||
|
||||
Returns:
|
||||
List of orders matching the criteria
|
||||
"""
|
||||
endpoint = "/orders"
|
||||
params = {"limit": limit}
|
||||
|
||||
if status:
|
||||
params["status"] = status
|
||||
if start_date:
|
||||
params["startDate"] = start_date.isoformat()
|
||||
if end_date:
|
||||
params["endDate"] = end_date.isoformat()
|
||||
|
||||
response = await self._make_request("GET", endpoint, params=params)
|
||||
return response.get("results", [])
|
||||
|
||||
async def get_order_details(self, order_id: str) -> Dict:
|
||||
"""
|
||||
Get detailed information about a specific order
|
||||
|
||||
Args:
|
||||
order_id: TCGPlayer order ID
|
||||
|
||||
Returns:
|
||||
Detailed order information
|
||||
"""
|
||||
endpoint = f"/orders/{order_id}"
|
||||
response = await self._make_request("GET", endpoint)
|
||||
return response
|
||||
|
||||
async def get_order_items(self, order_id: str) -> List[Dict]:
|
||||
"""
|
||||
Get items in a specific order
|
||||
|
||||
Args:
|
||||
order_id: TCGPlayer order ID
|
||||
|
||||
Returns:
|
||||
List of items in the order
|
||||
"""
|
||||
endpoint = f"/orders/{order_id}/items"
|
||||
response = await self._make_request("GET", endpoint)
|
||||
return response.get("results", [])
|
||||
|
||||
async def get_order_status(self, order_id: str) -> Dict:
|
||||
"""
|
||||
Get the current status of an order
|
||||
|
||||
Args:
|
||||
order_id: TCGPlayer order ID
|
||||
|
||||
Returns:
|
||||
Order status information
|
||||
"""
|
||||
endpoint = f"/orders/{order_id}/status"
|
||||
response = await self._make_request("GET", endpoint)
|
||||
return response
|
||||
|
||||
async def update_order_status(
|
||||
self,
|
||||
order_id: str,
|
||||
status: str,
|
||||
tracking_number: Optional[str] = None
|
||||
) -> Dict:
|
||||
"""
|
||||
Update the status of an order
|
||||
|
||||
Args:
|
||||
order_id: TCGPlayer order ID
|
||||
status: New status for the order
|
||||
tracking_number: Optional tracking number for shipping
|
||||
|
||||
Returns:
|
||||
Updated order information
|
||||
"""
|
||||
endpoint = f"/orders/{order_id}/status"
|
||||
data = {"status": status}
|
||||
if tracking_number:
|
||||
data["trackingNumber"] = tracking_number
|
||||
|
||||
response = await self._make_request("PUT", endpoint, data=data)
|
||||
return response
|
146
app/services/file_processing_service.py
Normal file
146
app/services/file_processing_service.py
Normal file
@ -0,0 +1,146 @@
|
||||
from typing import Optional, List, Dict
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import transaction
|
||||
from app.models.inventory import Inventory
|
||||
from app.models.tcgplayer_product import TCGPlayerProduct
|
||||
from app.services.inventory_service import InventoryService
|
||||
|
||||
class FileProcessingService:
|
||||
def __init__(self, cache_dir: str = "app/data/cache/tcgplayer"):
|
||||
self.cache_dir = cache_dir
|
||||
self.inventory_service = InventoryService()
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
def _get_cache_path(self, filename: str) -> str:
|
||||
return os.path.join(self.cache_dir, filename)
|
||||
|
||||
async def _cache_export(self, file_bytes: bytes, export_type: str):
|
||||
cache_path = self._get_cache_path(f"{export_type}_export.csv")
|
||||
with open(cache_path, 'wb') as f:
|
||||
f.write(file_bytes)
|
||||
|
||||
async def _load_cached_export(self, export_type: str) -> Optional[bytes]:
|
||||
cache_path = self._get_cache_path(f"{export_type}_export.csv")
|
||||
if os.path.exists(cache_path):
|
||||
with open(cache_path, 'rb') as f:
|
||||
return f.read()
|
||||
return None
|
||||
|
||||
async def process_tcgplayer_export(self, db: Session, file_bytes: bytes, export_type: str = "live", use_cache: bool = False) -> dict:
|
||||
"""
|
||||
Process a TCGPlayer export file and load it into the inventory table.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
file_bytes: The downloaded file content as bytes
|
||||
export_type: Type of export (staged, live, pricing)
|
||||
use_cache: Whether to use cached export file for development
|
||||
|
||||
Returns:
|
||||
dict: Processing statistics
|
||||
"""
|
||||
stats = {
|
||||
"total_rows": 0,
|
||||
"processed_rows": 0,
|
||||
"errors": 0,
|
||||
"error_messages": []
|
||||
}
|
||||
|
||||
try:
|
||||
# For development, use cached file if available
|
||||
if use_cache:
|
||||
cached_bytes = await self._load_cached_export(export_type)
|
||||
if cached_bytes:
|
||||
file_bytes = cached_bytes
|
||||
else:
|
||||
await self._cache_export(file_bytes, export_type)
|
||||
|
||||
# Convert bytes to string and create a file-like object
|
||||
file_content = file_bytes.decode('utf-8')
|
||||
file_like = io.StringIO(file_content)
|
||||
|
||||
# Read CSV file
|
||||
csv_reader = csv.DictReader(file_like)
|
||||
|
||||
with transaction(db):
|
||||
for row in csv_reader:
|
||||
stats["total_rows"] += 1
|
||||
try:
|
||||
# Process each row and create/update inventory item in database
|
||||
inventory_data = self._map_tcgplayer_row_to_inventory(row)
|
||||
tcgplayer_id = inventory_data["tcgplayer_id"]
|
||||
|
||||
# Check if inventory item already exists
|
||||
existing_item = self.inventory_service.get_by_tcgplayer_id(db, tcgplayer_id)
|
||||
|
||||
# Find matching TCGPlayer product
|
||||
product_id = int(tcgplayer_id) if tcgplayer_id.isdigit() else None
|
||||
if product_id:
|
||||
tcg_product = db.query(TCGPlayerProduct).filter(TCGPlayerProduct.product_id == product_id).first()
|
||||
if tcg_product:
|
||||
# Update inventory data with product information if available
|
||||
inventory_data.update({
|
||||
"product_name": tcg_product.name,
|
||||
"photo_url": tcg_product.image_url,
|
||||
"rarity": tcg_product.ext_rarity,
|
||||
"number": tcg_product.ext_number
|
||||
})
|
||||
|
||||
if existing_item:
|
||||
# Update existing item
|
||||
self.inventory_service.update(db, existing_item, inventory_data)
|
||||
else:
|
||||
# Create new item
|
||||
self.inventory_service.create(db, inventory_data)
|
||||
|
||||
stats["processed_rows"] += 1
|
||||
except Exception as e:
|
||||
stats["errors"] += 1
|
||||
stats["error_messages"].append(f"Error processing row {stats['total_rows']}: {str(e)}")
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to process TCGPlayer export: {str(e)}")
|
||||
|
||||
def _map_tcgplayer_row_to_inventory(self, row: dict) -> dict:
|
||||
"""
|
||||
Map TCGPlayer export row to inventory model fields.
|
||||
"""
|
||||
def safe_float(value: str) -> float:
|
||||
"""Convert string to float, returning 0.0 for empty strings or invalid values"""
|
||||
try:
|
||||
return float(value) if value else 0.0
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
def safe_int(value: str) -> int:
|
||||
"""Convert string to int, returning 0 for empty strings or invalid values"""
|
||||
try:
|
||||
return int(value) if value else 0
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
return {
|
||||
"tcgplayer_id": row.get("TCGplayer Id", ""),
|
||||
"product_line": row.get("Product Line", ""),
|
||||
"set_name": row.get("Set Name", ""),
|
||||
"product_name": row.get("Product Name", ""),
|
||||
"title": row.get("Title", ""),
|
||||
"number": row.get("Number", ""),
|
||||
"rarity": row.get("Rarity", ""),
|
||||
"condition": row.get("Condition", ""),
|
||||
"tcg_market_price": safe_float(row.get("TCG Market Price", "")),
|
||||
"tcg_direct_low": safe_float(row.get("TCG Direct Low", "")),
|
||||
"tcg_low_price_with_shipping": safe_float(row.get("TCG Low Price With Shipping", "")),
|
||||
"tcg_low_price": safe_float(row.get("TCG Low Price", "")),
|
||||
"total_quantity": safe_int(row.get("Total Quantity", "")),
|
||||
"add_to_quantity": safe_int(row.get("Add to Quantity", "")),
|
||||
"tcg_marketplace_price": safe_float(row.get("TCG Marketplace Price", "")),
|
||||
"photo_url": row.get("Photo URL", "")
|
||||
}
|
63
app/services/inventory_service.py
Normal file
63
app/services/inventory_service.py
Normal file
@ -0,0 +1,63 @@
|
||||
from typing import List, Optional, Dict
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.inventory import Inventory
|
||||
from app.services.base_service import BaseService
|
||||
|
||||
class InventoryService(BaseService[Inventory]):
|
||||
def __init__(self):
|
||||
super().__init__(Inventory)
|
||||
|
||||
def create(self, db: Session, obj_in: Dict) -> Inventory:
|
||||
"""
|
||||
Create a new inventory item in the database.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
obj_in: Dictionary containing inventory data
|
||||
|
||||
Returns:
|
||||
Inventory: The created inventory object
|
||||
"""
|
||||
return super().create(db, obj_in)
|
||||
|
||||
def update(self, db: Session, db_obj: Inventory, obj_in: Dict) -> Inventory:
|
||||
"""
|
||||
Update an existing inventory item in the database.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_obj: The inventory object to update
|
||||
obj_in: Dictionary containing updated inventory data
|
||||
|
||||
Returns:
|
||||
Inventory: The updated inventory object
|
||||
"""
|
||||
return super().update(db, db_obj, obj_in)
|
||||
|
||||
def get_by_tcgplayer_id(self, db: Session, tcgplayer_id: str) -> Optional[Inventory]:
|
||||
"""
|
||||
Get an inventory item by its TCGPlayer ID.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
tcgplayer_id: The TCGPlayer ID to find
|
||||
|
||||
Returns:
|
||||
Optional[Inventory]: The inventory item if found, None otherwise
|
||||
"""
|
||||
return db.query(self.model).filter(self.model.tcgplayer_id == tcgplayer_id).first()
|
||||
|
||||
def get_by_set(self, db: Session, set_name: str, skip: int = 0, limit: int = 100) -> List[Inventory]:
|
||||
"""
|
||||
Get all inventory items from a specific set.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
set_name: The name of the set to filter by
|
||||
skip: Number of records to skip (for pagination)
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
List[Inventory]: List of inventory items from the specified set
|
||||
"""
|
||||
return db.query(self.model).filter(self.model.set_name == set_name).offset(skip).limit(limit).all()
|
58
app/services/order_service.py
Normal file
58
app/services/order_service.py
Normal file
@ -0,0 +1,58 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from app.services.base_service import BaseService
|
||||
from app.models.order import Order, OrderCard
|
||||
from app.models.card import Card
|
||||
|
||||
class OrderService(BaseService):
|
||||
def __init__(self):
|
||||
super().__init__(Order)
|
||||
|
||||
def create_order_with_cards(self, db: Session, order_data: dict, card_ids: list[int]) -> Order:
|
||||
"""
|
||||
Create a new order with associated cards.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
order_data: Dictionary containing order details
|
||||
card_ids: List of card IDs to associate with the order
|
||||
|
||||
Returns:
|
||||
The created Order object
|
||||
"""
|
||||
# Create the order
|
||||
order = Order(**order_data)
|
||||
db.add(order)
|
||||
db.flush() # Get the order ID
|
||||
|
||||
# Associate cards with the order
|
||||
for card_id in card_ids:
|
||||
card = db.query(Card).filter(Card.id == card_id).first()
|
||||
if not card:
|
||||
raise ValueError(f"Card with ID {card_id} not found")
|
||||
|
||||
order_card = OrderCard(order_id=order.id, card_id=card_id)
|
||||
db.add(order_card)
|
||||
|
||||
db.commit()
|
||||
db.refresh(order)
|
||||
return order
|
||||
|
||||
def get_orders_with_cards(self, db: Session, skip: int = 0, limit: int = 10) -> list[Order]:
|
||||
"""
|
||||
Get orders with their associated cards.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
skip: Number of records to skip
|
||||
limit: Maximum number of records to return
|
||||
|
||||
Returns:
|
||||
List of Order objects with their associated cards
|
||||
"""
|
||||
orders = db.query(Order).offset(skip).limit(limit).all()
|
||||
|
||||
# Eager load the cards for each order
|
||||
for order in orders:
|
||||
order.cards = db.query(Card).join(OrderCard).filter(OrderCard.order_id == order.id).all()
|
||||
|
||||
return orders
|
54
app/services/scheduler/scheduler_service.py
Normal file
54
app/services/scheduler/scheduler_service.py
Normal file
@ -0,0 +1,54 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import SessionLocal, transaction
|
||||
from app.services.external_api.tcgplayer.tcgplayer_inventory_service import TCGPlayerInventoryService
|
||||
from app.services.file_processing_service import FileProcessingService
|
||||
from app.services.scheduler.base_scheduler import BaseScheduler
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SchedulerService:
|
||||
def __init__(self):
|
||||
self.tcgplayer_service = TCGPlayerInventoryService()
|
||||
self.file_processor = FileProcessingService()
|
||||
self.scheduler = BaseScheduler()
|
||||
|
||||
async def process_tcgplayer_export(self, export_type: str = "live", use_cache: bool = False):
|
||||
"""
|
||||
Process TCGPlayer export as a scheduled task.
|
||||
|
||||
Args:
|
||||
export_type: Type of export to process (staged, live, or pricing)
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
logger.info(f"Starting scheduled TCGPlayer export processing for {export_type}")
|
||||
|
||||
# Download the file
|
||||
file_bytes = await self.tcgplayer_service.get_tcgplayer_export(export_type)
|
||||
|
||||
# Process the file and load into database
|
||||
with transaction(db):
|
||||
stats = await self.file_processor.process_tcgplayer_export(db, export_type=export_type, file_bytes=file_bytes, use_cache=use_cache)
|
||||
|
||||
logger.info(f"Completed TCGPlayer export processing: {stats}")
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing TCGPlayer export: {str(e)}")
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
async def start_scheduled_tasks(self):
|
||||
"""Start all scheduled tasks"""
|
||||
# Schedule TCGPlayer export processing to run daily at 2 AM
|
||||
await self.scheduler.schedule_task(
|
||||
task_name="process_tcgplayer_export",
|
||||
func=self.process_tcgplayer_export,
|
||||
interval_seconds=24 * 60 * 60, # 24 hours
|
||||
export_type="live"
|
||||
)
|
||||
|
||||
self.scheduler.start()
|
||||
logger.info("All scheduled tasks started")
|
Reference in New Issue
Block a user