sex
This commit is contained in:
commit
1c00ea8569
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.db-trace
|
||||||
|
*.db-wal
|
||||||
|
*.db-lock
|
||||||
|
*.db-shm
|
||||||
|
*.db-trace
|
||||||
|
|
||||||
|
__pycache__
|
||||||
|
_cookie.py
|
41
app/db/database.py
Normal file
41
app/db/database.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///database.db")
|
||||||
|
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
"""Dependency to get a database session. Used for simple, single service operations."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def transaction(db: Session):
|
||||||
|
"""Context manager to handle database transactions. Used for service operations that require multiple database operations."""
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
db.commit()
|
||||||
|
except Exception as e:
|
||||||
|
db.rollback()
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
45
app/main.py
Normal file
45
app/main.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
import uvicorn
|
||||||
|
import logging
|
||||||
|
from routes import routes
|
||||||
|
from db.database import init_db
|
||||||
|
from app.models.card import Card # Assuming you have a Card model
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
|
handlers=[logging.StreamHandler(), logging.FileHandler("app.log")],)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="CCR Cards Management API",
|
||||||
|
description="API for managing CCR Cards Inventory, Orders, and more.",
|
||||||
|
version="0.1.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(routes.router)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def on_startup():
|
||||||
|
init_db()
|
||||||
|
logger.info("Database initialized successfully")
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def on_shutdown():
|
||||||
|
logger.info("Database connection closed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000, reload=True)
|
37
app/models/box.py
Normal file
37
app/models/box.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.database import Base
|
||||||
|
|
||||||
|
class Box(Base):
|
||||||
|
__tablename__ = "boxes"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
product_id = Column(Integer)
|
||||||
|
type = Column(String)
|
||||||
|
set_code = Column(String)
|
||||||
|
sku = Column(Integer)
|
||||||
|
name = Column(String)
|
||||||
|
expected_number_of_cards = Column(Integer)
|
||||||
|
description = Column(String)
|
||||||
|
image_url = Column(String)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
cards = relationship("Card", back_populates="box")
|
||||||
|
open_boxes = relationship("OpenBox", back_populates="box")
|
||||||
|
|
||||||
|
class OpenBox(Base):
|
||||||
|
__tablename__ = "open_boxes"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
box_id = Column(Integer, ForeignKey("boxes.id"))
|
||||||
|
number_of_cards = Column(Integer)
|
||||||
|
date_opened = Column(DateTime(timezone=True))
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
box = relationship("Box", back_populates="open_boxes")
|
||||||
|
cards = relationship("Card", back_populates="open_box")
|
23
app/models/card.py
Normal file
23
app/models/card.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Card(Base):
|
||||||
|
__tablename__ = "cards"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String, index=True)
|
||||||
|
rarity = Column(String)
|
||||||
|
set_name = Column(String)
|
||||||
|
price = Column(Float)
|
||||||
|
quantity = Column(Integer, default=0)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
orders = relationship("Order", secondary="order_cards", back_populates="cards")
|
21
app/models/file.py
Normal file
21
app/models/file.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import Column, Integer, String, DateTime
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class File(Base):
|
||||||
|
__tablename__ = "files"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String)
|
||||||
|
type = Column(String)
|
||||||
|
path = Column(String)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
|
||||||
|
|
20
app/models/game.py
Normal file
20
app/models/game.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import Column, Integer, String, DateTime
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.database import Base
|
||||||
|
|
||||||
|
class Game(Base):
|
||||||
|
__tablename__ = "games"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String)
|
||||||
|
description = Column(String)
|
||||||
|
image_url = Column(String)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
boxes = relationship("Box", back_populates="game")
|
0
app/routes/__init__.py
Normal file
0
app/routes/__init__.py
Normal file
176
app/routes/routes.py
Normal file
176
app/routes/routes.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.db.database import get_db
|
||||||
|
from app.models.file import File, FileCreate, FileUpdate, FileDelete, FileList
|
||||||
|
from app.models.box import Box, BoxCreate, BoxUpdate, BoxDelete, BoxList, OpenBox, OpenBoxCreate, OpenBoxUpdate, OpenBoxDelete, OpenBoxList
|
||||||
|
from app.models.game import Game, GameCreate, GameUpdate, GameDelete, GameList
|
||||||
|
from app.models.card import Card, CardCreate, CardUpdate, CardDelete, CardList
|
||||||
|
from app.services import CardService, OrderService
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
# Initialize services
|
||||||
|
card_service = CardService()
|
||||||
|
order_service = OrderService()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Health Check & Root Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
@router.get("/")
|
||||||
|
async def root():
|
||||||
|
return {"message": "CCR Cards Management API is running."}
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Card Management Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
@router.get("/cards", response_model=CardList)
|
||||||
|
async def get_cards(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
page: int = 1,
|
||||||
|
limit: int = 10,
|
||||||
|
type: str = None,
|
||||||
|
id: int = None
|
||||||
|
):
|
||||||
|
skip = (page - 1) * limit
|
||||||
|
cards = card_service.get_all(db, skip=skip, limit=limit)
|
||||||
|
total = db.query(Card).count()
|
||||||
|
return {
|
||||||
|
"cards": cards,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.post("/cards", response_model=Card)
|
||||||
|
async def create_card(card: CardCreate, db: Session = Depends(get_db)):
|
||||||
|
try:
|
||||||
|
created_card = card_service.create(db, card.dict())
|
||||||
|
return created_card
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
@router.put("/cards/{card_id}", response_model=Card)
|
||||||
|
async def update_card(card_id: int, card: CardUpdate, db: Session = Depends(get_db)):
|
||||||
|
db_card = card_service.get(db, card_id)
|
||||||
|
if not db_card:
|
||||||
|
raise HTTPException(status_code=404, detail="Card not found")
|
||||||
|
try:
|
||||||
|
updated_card = card_service.update(db, db_card, card.dict(exclude_unset=True))
|
||||||
|
return updated_card
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
@router.delete("/cards/{card_id}", response_model=CardDelete)
|
||||||
|
async def delete_card(card_id: int, db: Session = Depends(get_db)):
|
||||||
|
success = card_service.delete(db, card_id)
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Card not found")
|
||||||
|
return {"message": "Card deleted successfully"}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Order Management Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
@router.post("/orders")
|
||||||
|
async def create_order(order_data: dict, card_ids: list[int], db: Session = Depends(get_db)):
|
||||||
|
try:
|
||||||
|
order = order_service.create_order_with_cards(db, order_data, card_ids)
|
||||||
|
return order
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get("/orders")
|
||||||
|
async def get_orders(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
page: int = 1,
|
||||||
|
limit: int = 10
|
||||||
|
):
|
||||||
|
skip = (page - 1) * limit
|
||||||
|
orders = order_service.get_orders_with_cards(db, skip=skip, limit=limit)
|
||||||
|
return {
|
||||||
|
"orders": orders,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# File Management Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
@router.get("/files", response_model=FileList)
|
||||||
|
async def get_files(page: int = 1, limit: int = 10, type: str = None, id: int = None):
|
||||||
|
return {"files": [], "total": 0, "page": page, "limit": limit}
|
||||||
|
|
||||||
|
@router.post("/files", response_model=File)
|
||||||
|
async def create_file(file: FileCreate):
|
||||||
|
return {"message": "File created successfully"}
|
||||||
|
|
||||||
|
@router.put("/files/{file_id}", response_model=File)
|
||||||
|
async def update_file(file_id: int, file: FileUpdate):
|
||||||
|
return {"message": "File updated successfully"}
|
||||||
|
|
||||||
|
@router.delete("/files/{file_id}", response_model=FileDelete)
|
||||||
|
async def delete_file(file_id: int):
|
||||||
|
return {"message": "File deleted successfully"}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Box Management Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
@router.get("/boxes", response_model=BoxList)
|
||||||
|
async def get_boxes(page: int = 1, limit: int = 10, type: str = None, id: int = None):
|
||||||
|
return {"boxes": [], "total": 0, "page": page, "limit": limit}
|
||||||
|
|
||||||
|
@router.post("/boxes", response_model=Box)
|
||||||
|
async def create_box(box: BoxCreate):
|
||||||
|
return {"message": "Box created successfully"}
|
||||||
|
|
||||||
|
@router.put("/boxes/{box_id}", response_model=Box)
|
||||||
|
async def update_box(box_id: int, box: BoxUpdate):
|
||||||
|
return {"message": "Box updated successfully"}
|
||||||
|
|
||||||
|
@router.delete("/boxes/{box_id}", response_model=BoxDelete)
|
||||||
|
async def delete_box(box_id: int):
|
||||||
|
return {"message": "Box deleted successfully"}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Open Box Management Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
@router.get("/open_boxes", response_model=OpenBoxList)
|
||||||
|
async def get_open_boxes(page: int = 1, limit: int = 10, type: str = None, id: int = None):
|
||||||
|
return {"open_boxes": [], "total": 0, "page": page, "limit": limit}
|
||||||
|
|
||||||
|
@router.post("/open_boxes", response_model=OpenBox)
|
||||||
|
async def create_open_box(open_box: OpenBoxCreate):
|
||||||
|
return {"message": "Open box created successfully"}
|
||||||
|
|
||||||
|
@router.put("/open_boxes/{open_box_id}", response_model=OpenBox)
|
||||||
|
async def update_open_box(open_box_id: int, open_box: OpenBoxUpdate):
|
||||||
|
return {"message": "Open box updated successfully"}
|
||||||
|
|
||||||
|
@router.delete("/open_boxes/{open_box_id}", response_model=OpenBoxDelete)
|
||||||
|
async def delete_open_box(open_box_id: int):
|
||||||
|
return {"message": "Open box deleted successfully"}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Game Management Endpoints
|
||||||
|
# ============================================================================
|
||||||
|
@router.get("/games", response_model=GameList)
|
||||||
|
async def get_games(page: int = 1, limit: int = 10, type: str = None, id: int = None):
|
||||||
|
return {"games": [], "total": 0, "page": page, "limit": limit}
|
||||||
|
|
||||||
|
@router.post("/games", response_model=Game)
|
||||||
|
async def create_game(game: GameCreate):
|
||||||
|
return {"message": "Game created successfully"}
|
||||||
|
|
||||||
|
@router.put("/games/{game_id}", response_model=Game)
|
||||||
|
async def update_game(game_id: int, game: GameUpdate):
|
||||||
|
return {"message": "Game updated successfully"}
|
||||||
|
|
||||||
|
@router.delete("/games/{game_id}", response_model=GameDelete)
|
||||||
|
async def delete_game(game_id: int):
|
||||||
|
return {"message": "Game deleted successfully"}
|
||||||
|
|
81
app/schemas/box.py
Normal file
81
app/schemas/box.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Base schema with common attributes
|
||||||
|
class BoxBase(BaseModel):
|
||||||
|
product_id: int
|
||||||
|
type: str
|
||||||
|
set_code: str
|
||||||
|
sku: int
|
||||||
|
name: str
|
||||||
|
expected_number_of_cards: int
|
||||||
|
description: str
|
||||||
|
image_url: str
|
||||||
|
|
||||||
|
# Schema for creating a new box
|
||||||
|
class BoxCreate(BoxBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Schema for updating a box
|
||||||
|
class BoxUpdate(BaseModel):
|
||||||
|
product_id: Optional[int] = None
|
||||||
|
type: Optional[str] = None
|
||||||
|
set_code: Optional[str] = None
|
||||||
|
sku: Optional[int] = None
|
||||||
|
name: Optional[str] = None
|
||||||
|
expected_number_of_cards: Optional[int] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
image_url: Optional[str] = None
|
||||||
|
|
||||||
|
# Schema for reading a box
|
||||||
|
class Box(BoxBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
cards: List["Card"] = []
|
||||||
|
open_boxes: List["OpenBox"] = []
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
# Schema for deleting a box
|
||||||
|
class BoxDelete(BaseModel):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
# Schema for listing boxes
|
||||||
|
class BoxList(BaseModel):
|
||||||
|
boxes: List[Box]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
# OpenBox schemas
|
||||||
|
class OpenBoxBase(BaseModel):
|
||||||
|
box_id: int
|
||||||
|
number_of_cards: int
|
||||||
|
date_opened: datetime
|
||||||
|
|
||||||
|
class OpenBoxCreate(OpenBoxBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class OpenBoxUpdate(BaseModel):
|
||||||
|
number_of_cards: Optional[int] = None
|
||||||
|
date_opened: Optional[datetime] = None
|
||||||
|
|
||||||
|
class OpenBox(OpenBoxBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
box: Optional[Box] = None
|
||||||
|
cards: List["Card"] = []
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
class OpenBoxDelete(BaseModel):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
class OpenBoxList(BaseModel):
|
||||||
|
open_boxes: List[OpenBox]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
limit: int
|
39
app/schemas/card.py
Normal file
39
app/schemas/card.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Base schema with common attributes
|
||||||
|
class CardBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
rarity: str
|
||||||
|
set_name: str
|
||||||
|
price: float
|
||||||
|
quantity: int = 0
|
||||||
|
|
||||||
|
# Schema for creating a new card
|
||||||
|
class CardCreate(CardBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Schema for updating a card
|
||||||
|
class CardUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
rarity: Optional[str] = None
|
||||||
|
set_name: Optional[str] = None
|
||||||
|
price: Optional[float] = None
|
||||||
|
quantity: Optional[int] = None
|
||||||
|
|
||||||
|
# Schema for reading a card (includes id and relationships)
|
||||||
|
class Card(CardBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
orders: List["Order"] = []
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
# Schema for listing cards
|
||||||
|
class CardList(BaseModel):
|
||||||
|
cards: List[Card]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
limit: int
|
38
app/schemas/file.py
Normal file
38
app/schemas/file.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Base schema with common attributes
|
||||||
|
class FileBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
path: str
|
||||||
|
|
||||||
|
# Schema for creating a new file
|
||||||
|
class FileCreate(FileBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Schema for updating a file
|
||||||
|
class FileUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
type: Optional[str] = None
|
||||||
|
path: Optional[str] = None
|
||||||
|
|
||||||
|
# Schema for reading a file
|
||||||
|
class File(FileBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
# Schema for deleting a file
|
||||||
|
class FileDelete(BaseModel):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
# Schema for listing files
|
||||||
|
class FileList(BaseModel):
|
||||||
|
files: List[File]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
limit: int
|
39
app/schemas/game.py
Normal file
39
app/schemas/game.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Base schema with common attributes
|
||||||
|
class GameBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
image_url: str
|
||||||
|
|
||||||
|
# Schema for creating a new game
|
||||||
|
class GameCreate(GameBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Schema for updating a game
|
||||||
|
class GameUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
image_url: Optional[str] = None
|
||||||
|
|
||||||
|
# Schema for reading a game
|
||||||
|
class Game(GameBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
boxes: List["Box"] = []
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
# Schema for deleting a game
|
||||||
|
class GameDelete(BaseModel):
|
||||||
|
id: int
|
||||||
|
|
||||||
|
# Schema for listing games
|
||||||
|
class GameList(BaseModel):
|
||||||
|
games: List[Game]
|
||||||
|
total: int
|
||||||
|
page: int
|
||||||
|
limit: int
|
5
app/services/__init__.py
Normal file
5
app/services/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from app.services.base_service import BaseService
|
||||||
|
from app.services.card_service import CardService
|
||||||
|
from app.services.service_registry import ServiceRegistry
|
||||||
|
|
||||||
|
__all__ = ["BaseService", "CardService", "ServiceRegistry"]
|
45
app/services/base_service.py
Normal file
45
app/services/base_service.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
from typing import Type, TypeVar, Generic, List, Optional, Any
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.db.database import Base
|
||||||
|
from app.services.service_registry import ServiceRegistry
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
class BaseService(Generic[T]):
|
||||||
|
def __init__(self, model: Type[T]):
|
||||||
|
self.model = model
|
||||||
|
# Register the service instance
|
||||||
|
ServiceRegistry.register(self.__class__.__name__, self)
|
||||||
|
|
||||||
|
def get(self, db: Session, id: int) -> Optional[T]:
|
||||||
|
return db.query(self.model).filter(self.model.id == id).first()
|
||||||
|
|
||||||
|
def get_all(self, db: Session, skip: int = 0, limit: int = 100) -> List[T]:
|
||||||
|
return db.query(self.model).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
def create(self, db: Session, obj_in: dict) -> T:
|
||||||
|
db_obj = self.model(**obj_in)
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def update(self, db: Session, db_obj: T, obj_in: dict) -> T:
|
||||||
|
for field in obj_in:
|
||||||
|
if hasattr(db_obj, field):
|
||||||
|
setattr(db_obj, field, obj_in[field])
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def delete(self, db: Session, id: int) -> bool:
|
||||||
|
obj = db.query(self.model).filter(self.model.id == id).first()
|
||||||
|
if obj:
|
||||||
|
db.delete(obj)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_service(self, service_name: str) -> Any:
|
||||||
|
return ServiceRegistry.get(service_name)
|
17
app/services/card_service.py
Normal file
17
app/services/card_service.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.services.base_service import BaseService
|
||||||
|
from app.models.card import Card
|
||||||
|
|
||||||
|
class CardService(BaseService[Card]):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(Card)
|
||||||
|
|
||||||
|
def get_by_name(self, db: Session, name: str) -> Optional[Card]:
|
||||||
|
return db.query(self.model).filter(self.model.name == name).first()
|
||||||
|
|
||||||
|
def get_by_rarity(self, db: Session, rarity: str, skip: int = 0, limit: int = 100) -> List[Card]:
|
||||||
|
return db.query(self.model).filter(self.model.rarity == rarity).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
def get_by_set(self, db: Session, set_name: str, skip: int = 0, limit: int = 100) -> List[Card]:
|
||||||
|
return db.query(self.model).filter(self.model.set_name == set_name).offset(skip).limit(limit).all()
|
49
app/services/external_api/base_external_service.py
Normal file
49
app/services/external_api/base_external_service.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from typing import Any, Dict, Optional
|
||||||
|
import aiohttp
|
||||||
|
import logging
|
||||||
|
from app.services.service_registry import ServiceRegistry
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class BaseExternalService:
|
||||||
|
def __init__(self, base_url: str, api_key: Optional[str] = None):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.api_key = api_key
|
||||||
|
self.session = None
|
||||||
|
# Register the service instance
|
||||||
|
ServiceRegistry.register(self.__class__.__name__, self)
|
||||||
|
|
||||||
|
async def _get_session(self) -> aiohttp.ClientSession:
|
||||||
|
if self.session is None or self.session.closed:
|
||||||
|
self.session = aiohttp.ClientSession()
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
async def _make_request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
endpoint: str,
|
||||||
|
params: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, str]] = None,
|
||||||
|
data: Optional[Dict[str, Any]] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
session = await self._get_session()
|
||||||
|
url = f"{self.base_url}{endpoint}"
|
||||||
|
|
||||||
|
if self.api_key:
|
||||||
|
headers = headers or {}
|
||||||
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with session.request(method, url, params=params, headers=headers, json=data) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
return await response.json()
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
logger.error(f"API request failed: {str(e)}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error during API request: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
if self.session and not self.session.closed:
|
||||||
|
await self.session.close()
|
@ -0,0 +1,95 @@
|
|||||||
|
from typing import Any, Dict, Optional
|
||||||
|
import aiohttp
|
||||||
|
import logging
|
||||||
|
from app.services.external_api.base_external_service import BaseExternalService
|
||||||
|
from app.services.external_api.tcgplayer.tcgplayer_credentials import TCGPlayerCredentials
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class BaseTCGPlayerService(BaseExternalService):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
store_base_url="https://store.tcgplayer.com",
|
||||||
|
login_endpoint="/oauth/login",
|
||||||
|
pricing_endpoint="/Admin/Pricing",
|
||||||
|
staged_inventory_endpoint=self.pricing_endpoint + "/DownloadStagedInventoryExportCSV?type=Pricing",
|
||||||
|
live_inventory_endpoint=self.pricing_endpoint + "/DownloadMyExportCSV?type=Pricing"
|
||||||
|
)
|
||||||
|
self.credentials = TCGPlayerCredentials()
|
||||||
|
|
||||||
|
def _get_headers(self, method: str) -> Dict[str, str]:
|
||||||
|
"""Get headers based on the HTTP method"""
|
||||||
|
base_headers = {
|
||||||
|
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
|
||||||
|
'accept-language': 'en-US,en;q=0.8',
|
||||||
|
'priority': 'u=0, i',
|
||||||
|
'referer': 'https://store.tcgplayer.com/admin/pricing',
|
||||||
|
'sec-ch-ua': '"Not A(Brand";v="8", "Chromium";v="132", "Brave";v="132"',
|
||||||
|
'sec-ch-ua-mobile': '?0',
|
||||||
|
'sec-ch-ua-platform': '"macOS"',
|
||||||
|
'sec-fetch-dest': 'document',
|
||||||
|
'sec-fetch-mode': 'navigate',
|
||||||
|
'sec-fetch-site': 'same-origin',
|
||||||
|
'sec-fetch-user': '?1',
|
||||||
|
'sec-gpc': '1',
|
||||||
|
'upgrade-insecure-requests': '1',
|
||||||
|
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36'
|
||||||
|
}
|
||||||
|
|
||||||
|
if method == 'POST':
|
||||||
|
post_headers = {
|
||||||
|
'cache-control': 'max-age=0',
|
||||||
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
|
'origin': 'https://store.tcgplayer.com'
|
||||||
|
}
|
||||||
|
base_headers.update(post_headers)
|
||||||
|
|
||||||
|
return base_headers
|
||||||
|
|
||||||
|
async def _make_request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
endpoint: str,
|
||||||
|
params: Optional[Dict[str, Any]] = None,
|
||||||
|
headers: Optional[Dict[str, str]] = None,
|
||||||
|
data: Optional[Dict[str, Any]] = None,
|
||||||
|
auth_required: bool = True
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
session = await self._get_session()
|
||||||
|
url = f"{self.store_base_url}{endpoint}"
|
||||||
|
|
||||||
|
# Get the authentication cookie if required
|
||||||
|
if auth_required:
|
||||||
|
cookie = self.credentials.get_cookie()
|
||||||
|
if not cookie:
|
||||||
|
raise RuntimeError("TCGPlayer authentication cookie not set. Please set the cookie using TCGPlayerCredentials.set_cookie()")
|
||||||
|
|
||||||
|
# Get method-specific headers and update with any provided headers
|
||||||
|
request_headers = self._get_headers(method)
|
||||||
|
if headers:
|
||||||
|
request_headers.update(headers)
|
||||||
|
request_headers["Cookie"] = cookie
|
||||||
|
else:
|
||||||
|
request_headers = headers or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with session.request(method, url, params=params, headers=request_headers, json=data) as response:
|
||||||
|
if response.status == 401:
|
||||||
|
raise RuntimeError("TCGPlayer authentication failed. Cookie may be invalid or expired.")
|
||||||
|
response.raise_for_status()
|
||||||
|
return await response.json()
|
||||||
|
except aiohttp.ClientError as e:
|
||||||
|
logger.error(f"TCGPlayer API request failed: {str(e)}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error during TCGPlayer API request: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _get_session(self) -> aiohttp.ClientSession:
|
||||||
|
if self.session is None or self.session.closed:
|
||||||
|
self.session = aiohttp.ClientSession()
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
if self.session and not self.session.closed:
|
||||||
|
await self.session.close()
|
61
app/services/external_api/tcgplayer/tcgplayer_credentials.py
Normal file
61
app/services/external_api/tcgplayer/tcgplayer_credentials.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class TCGPlayerCredentials:
|
||||||
|
_instance = None
|
||||||
|
_credentials_file = Path.home() / ".tcgplayer" / "credentials.json"
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super(TCGPlayerCredentials, cls).__new__(cls)
|
||||||
|
cls._instance._initialize()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def _initialize(self):
|
||||||
|
"""Initialize the credentials manager"""
|
||||||
|
self._cookie = None
|
||||||
|
self._load_credentials()
|
||||||
|
|
||||||
|
def _load_credentials(self):
|
||||||
|
"""Load credentials from the credentials file"""
|
||||||
|
try:
|
||||||
|
if self._credentials_file.exists():
|
||||||
|
with open(self._credentials_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self._cookie = data.get('cookie')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading TCGPlayer credentials: {str(e)}")
|
||||||
|
|
||||||
|
def _save_credentials(self):
|
||||||
|
"""Save credentials to the credentials file"""
|
||||||
|
try:
|
||||||
|
# Create directory if it doesn't exist
|
||||||
|
self._credentials_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with open(self._credentials_file, 'w') as f:
|
||||||
|
json.dump({'cookie': self._cookie}, f)
|
||||||
|
|
||||||
|
# Set appropriate file permissions
|
||||||
|
self._credentials_file.chmod(0o600)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error saving TCGPlayer credentials: {str(e)}")
|
||||||
|
|
||||||
|
def set_cookie(self, cookie: str):
|
||||||
|
"""Set the authentication cookie"""
|
||||||
|
self._cookie = cookie
|
||||||
|
self._save_credentials()
|
||||||
|
|
||||||
|
def get_cookie(self) -> Optional[str]:
|
||||||
|
"""Get the authentication cookie"""
|
||||||
|
return self._cookie
|
||||||
|
|
||||||
|
def clear_credentials(self):
|
||||||
|
"""Clear stored credentials"""
|
||||||
|
self._cookie = None
|
||||||
|
if self._credentials_file.exists():
|
||||||
|
self._credentials_file.unlink()
|
@ -0,0 +1,18 @@
|
|||||||
|
from typing import Dict, List, Optional
|
||||||
|
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
|
||||||
|
|
||||||
|
class TCGPlayerInventoryService(BaseTCGPlayerService):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
async def get_inventory(self) -> List[Dict]:
|
||||||
|
"""Get inventory items"""
|
||||||
|
endpoint = "/inventory"
|
||||||
|
response = await self._make_request("GET", endpoint)
|
||||||
|
return response.get("results", [])
|
||||||
|
|
||||||
|
async def update_inventory(self, updates: List[Dict]) -> Dict:
|
||||||
|
"""Update inventory items"""
|
||||||
|
endpoint = "/inventory"
|
||||||
|
response = await self._make_request("PUT", endpoint, data=updates)
|
||||||
|
return response
|
106
app/services/external_api/tcgplayer/tcgplayer_order_service.py
Normal file
106
app/services/external_api/tcgplayer/tcgplayer_order_service.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
from typing import Dict, List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from app.services.external_api.tcgplayer.base_tcgplayer_service import BaseTCGPlayerService
|
||||||
|
|
||||||
|
class TCGPlayerOrderService(BaseTCGPlayerService):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
async def get_orders(
|
||||||
|
self,
|
||||||
|
status: Optional[str] = None,
|
||||||
|
start_date: Optional[datetime] = None,
|
||||||
|
end_date: Optional[datetime] = None,
|
||||||
|
limit: int = 100
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Get a list of orders with optional filtering
|
||||||
|
|
||||||
|
Args:
|
||||||
|
status: Filter by order status (e.g., "Shipped", "Processing")
|
||||||
|
start_date: Filter orders after this date
|
||||||
|
end_date: Filter orders before this date
|
||||||
|
limit: Maximum number of orders to return
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of orders matching the criteria
|
||||||
|
"""
|
||||||
|
endpoint = "/orders"
|
||||||
|
params = {"limit": limit}
|
||||||
|
|
||||||
|
if status:
|
||||||
|
params["status"] = status
|
||||||
|
if start_date:
|
||||||
|
params["startDate"] = start_date.isoformat()
|
||||||
|
if end_date:
|
||||||
|
params["endDate"] = end_date.isoformat()
|
||||||
|
|
||||||
|
response = await self._make_request("GET", endpoint, params=params)
|
||||||
|
return response.get("results", [])
|
||||||
|
|
||||||
|
async def get_order_details(self, order_id: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Get detailed information about a specific order
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order_id: TCGPlayer order ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Detailed order information
|
||||||
|
"""
|
||||||
|
endpoint = f"/orders/{order_id}"
|
||||||
|
response = await self._make_request("GET", endpoint)
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def get_order_items(self, order_id: str) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Get items in a specific order
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order_id: TCGPlayer order ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of items in the order
|
||||||
|
"""
|
||||||
|
endpoint = f"/orders/{order_id}/items"
|
||||||
|
response = await self._make_request("GET", endpoint)
|
||||||
|
return response.get("results", [])
|
||||||
|
|
||||||
|
async def get_order_status(self, order_id: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Get the current status of an order
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order_id: TCGPlayer order ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Order status information
|
||||||
|
"""
|
||||||
|
endpoint = f"/orders/{order_id}/status"
|
||||||
|
response = await self._make_request("GET", endpoint)
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def update_order_status(
|
||||||
|
self,
|
||||||
|
order_id: str,
|
||||||
|
status: str,
|
||||||
|
tracking_number: Optional[str] = None
|
||||||
|
) -> Dict:
|
||||||
|
"""
|
||||||
|
Update the status of an order
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order_id: TCGPlayer order ID
|
||||||
|
status: New status for the order
|
||||||
|
tracking_number: Optional tracking number for shipping
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated order information
|
||||||
|
"""
|
||||||
|
endpoint = f"/orders/{order_id}/status"
|
||||||
|
data = {"status": status}
|
||||||
|
if tracking_number:
|
||||||
|
data["trackingNumber"] = tracking_number
|
||||||
|
|
||||||
|
response = await self._make_request("PUT", endpoint, data=data)
|
||||||
|
return response
|
55
app/services/scheduler/base_scheduler.py
Normal file
55
app/services/scheduler/base_scheduler.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from typing import Callable, Dict, Any
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
|
import logging
|
||||||
|
from app.services.service_registry import ServiceRegistry
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class BaseScheduler:
|
||||||
|
def __init__(self):
|
||||||
|
self.scheduler = AsyncIOScheduler()
|
||||||
|
self.jobs: Dict[str, Any] = {}
|
||||||
|
ServiceRegistry.register(self.__class__.__name__, self)
|
||||||
|
|
||||||
|
async def schedule_task(
|
||||||
|
self,
|
||||||
|
task_name: str,
|
||||||
|
func: Callable,
|
||||||
|
interval_seconds: int,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
) -> None:
|
||||||
|
"""Schedule a task to run at regular intervals using APScheduler"""
|
||||||
|
if task_name in self.jobs:
|
||||||
|
logger.warning(f"Task {task_name} already exists. Removing existing job.")
|
||||||
|
self.jobs[task_name].remove()
|
||||||
|
|
||||||
|
job = self.scheduler.add_job(
|
||||||
|
func,
|
||||||
|
trigger=IntervalTrigger(seconds=interval_seconds),
|
||||||
|
args=args,
|
||||||
|
kwargs=kwargs,
|
||||||
|
id=task_name,
|
||||||
|
replace_existing=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.jobs[task_name] = job
|
||||||
|
logger.info(f"Scheduled task {task_name} to run every {interval_seconds} seconds")
|
||||||
|
|
||||||
|
def remove_task(self, task_name: str) -> None:
|
||||||
|
"""Remove a scheduled task"""
|
||||||
|
if task_name in self.jobs:
|
||||||
|
self.jobs[task_name].remove()
|
||||||
|
del self.jobs[task_name]
|
||||||
|
logger.info(f"Removed task {task_name}")
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Start the scheduler"""
|
||||||
|
self.scheduler.start()
|
||||||
|
logger.info("Scheduler started")
|
||||||
|
|
||||||
|
async def shutdown(self) -> None:
|
||||||
|
"""Shutdown the scheduler"""
|
||||||
|
self.scheduler.shutdown()
|
||||||
|
logger.info("Scheduler stopped")
|
18
app/services/service_registry.py
Normal file
18
app/services/service_registry.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
class ServiceRegistry:
|
||||||
|
_services: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls, name: str, service: Any) -> None:
|
||||||
|
cls._services[name] = service
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name: str) -> Any:
|
||||||
|
if name not in cls._services:
|
||||||
|
raise ValueError(f"Service {name} not found in registry")
|
||||||
|
return cls._services[name]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def clear(cls) -> None:
|
||||||
|
cls._services.clear()
|
Loading…
x
Reference in New Issue
Block a user