Complete backend verification: fix syntax errors, create __init__.py files, add setup_db.py, update docker-compose for two PostgreSQL containers
This commit is contained in:
@@ -12,7 +12,7 @@ Migrate from internal database to Dockerized PostgreSQL with mtgjson.com "All Pr
|
||||
│ │ Backend Container │───▶│ PostgreSQL Container │ │
|
||||
│ │ (FastAPI app) │ │ (MTG Data + App DB) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ - API endpoints │ │ - cockatrice_db (app) │ │
|
||||
│ │ - API endpoints │ │ - mtgonline_db (app) │ │
|
||||
│ │ - Auth system │ │ - mtgdata_db (mtgjson) │ │
|
||||
│ │ - Weekly refresh │ │ │ │
|
||||
│ └─────────────────────┘ └──────────────────────────┘ │
|
||||
@@ -37,7 +37,7 @@ Migrate from internal database to Dockerized PostgreSQL with mtgjson.com "All Pr
|
||||
### 3. Database Migration
|
||||
- [ ] Create migration script for mtgjson schema
|
||||
- [ ] Update models.py with MTG card models
|
||||
- [ ] Add multi-database support (cockatrice + mtgdata)
|
||||
- [ ] Add multi-database support (mtgonline + mtgdata)
|
||||
- [ ] Create schema for weekly refresh
|
||||
|
||||
### 4. Weekly Refresh Logic
|
||||
|
||||
+4
-4
@@ -17,7 +17,7 @@
|
||||
|
||||
```bash
|
||||
# Navigate to project root
|
||||
cd /home/user/wall-o/cockatrice-web
|
||||
cd /home/user/wall-o/mtgonline-web
|
||||
|
||||
# Start PostgreSQL and Redis
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
@@ -29,8 +29,8 @@ docker compose -f docker-compose.dev.yml ps
|
||||
**Expected output:**
|
||||
```
|
||||
NAME STATUS PORTS
|
||||
cockatrice-web-postgres-1 Up 0.0.0.0:5432->5432/tcp
|
||||
cockatrice-web-redis-1 Up 0.0.0.0:6379->6379/tcp
|
||||
mtgonline-web-postgres-1 Up 0.0.0.0:5432->5432/tcp
|
||||
mtgonline-web-redis-1 Up 0.0.0.0:6379->6379/tcp
|
||||
```
|
||||
|
||||
### 2. Create Virtual Environment and Install Backend Dependencies
|
||||
@@ -111,7 +111,7 @@ pytest --cov=app --cov-report=html
|
||||
|
||||
```bash
|
||||
# Stop Docker services
|
||||
cd /home/user/wall-o/cockatrice-web
|
||||
cd /home/user/wall-o/mtgonline-web
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
|
||||
# Deactivate virtual environment (if in backend directory)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Cockatrice Web Application
|
||||
# MTG Online Web Application
|
||||
|
||||
A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathering platform.
|
||||
A modern web-based implementation of the MTG Online multiplayer Magic: The Gathering platform.
|
||||
|
||||
## Features
|
||||
|
||||
- **User Authentication**: Secure JWT-based authentication with bcrypt password hashing
|
||||
- **Deck Building**: Full-featured deck editor with import/export in multiple formats
|
||||
- **Real-Time Multiplayer**: WebSocket-based game server for live gameplay
|
||||
- **Protocol Compatibility**: Compatible with Cockatrice protocol buffer messages
|
||||
- **Protocol Compatibility**: Compatible with MTG Online protocol buffer messages
|
||||
- **Card Database**: Integration with MTJSON for comprehensive card data
|
||||
- **Admin Tools**: Comprehensive moderation and administration dashboard
|
||||
|
||||
@@ -33,8 +33,8 @@ A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathe
|
||||
1. **Clone the repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/cockatrice-web.git
|
||||
cd cockatrice-web
|
||||
git clone https://github.com/yourusername/mtgonline-web.git
|
||||
cd mtgonline-web
|
||||
```
|
||||
|
||||
2. **Create a virtual environment**
|
||||
@@ -62,7 +62,7 @@ cp .env.example .env
|
||||
|
||||
```bash
|
||||
# Create PostgreSQL database
|
||||
createdb cockatrice
|
||||
createdb mtgonline
|
||||
|
||||
# Run migrations (when Alembic is set up)
|
||||
alembic upgrade head
|
||||
@@ -81,7 +81,7 @@ Open http://localhost:8000/docs to view the FastAPI Swagger UI.
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
cockatrice-web/
|
||||
mtgonline-web/
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── core/ # Core configuration and utilities
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
# Cockatrice Web — Project Roadmap
|
||||
# MTG Online Web — Project Roadmap
|
||||
|
||||
## Overview
|
||||
|
||||
A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathering platform. Built with Python/FastAPI backend and React/TypeScript frontend to replace the legacy C++/Qt desktop client.
|
||||
A modern web-based implementation of the MTG Online multiplayer Magic: The Gathering platform. Built with Python/FastAPI backend and React/TypeScript frontend to replace the legacy C++/Qt desktop client.
|
||||
|
||||
## Phase 1: Backend Foundation ✅ (COMPLETED)
|
||||
|
||||
@@ -34,7 +34,7 @@ A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathe
|
||||
- [x] WebSocket game server
|
||||
- [x] Deck parser (plain text + native XML)
|
||||
- [x] Card database service (MTJSON integration)
|
||||
- [x] Protocol constants (Cockatrice protocol compatibility)
|
||||
- [x] Protocol constants (MTG Online protocol compatibility)
|
||||
|
||||
### 1.5 Testing
|
||||
- [x] Pytest configuration with async support
|
||||
|
||||
+16
-16
@@ -1,13 +1,13 @@
|
||||
# Statement of Intent — Cockatrice Web
|
||||
# Statement of Intent — MTG Online Web
|
||||
|
||||
## Project Title
|
||||
**Cockatrice Web** — A modern, web-based implementation of the Cockatrice multiplayer Magic: The Gathering platform.
|
||||
**MTG Online Web** — A modern, web-based implementation of the MTG Online multiplayer Magic: The Gathering platform.
|
||||
|
||||
## Vision Statement
|
||||
To build a fully-featured, open-source multiplayer Magic: The Gathering platform that runs entirely in modern web browsers, eliminating the need for desktop software installations while maintaining compatibility with the existing Cockatrice ecosystem.
|
||||
To build a fully-featured, open-source multiplayer Magic: The Gathering platform that runs entirely in modern web browsers, eliminating the need for desktop software installations while maintaining compatibility with the existing MTG Online ecosystem.
|
||||
|
||||
## Problem Statement
|
||||
The original Cockatrice application requires:
|
||||
The original MTG Online application requires:
|
||||
- Desktop software installation (Windows, macOS, Linux)
|
||||
- Manual updates and dependency management
|
||||
- Complex setup for new users
|
||||
@@ -19,7 +19,7 @@ A Progressive Web Application (PWA) that provides:
|
||||
- **Zero Installation**: Runs directly in any modern web browser
|
||||
- **Cross-Platform**: Works on desktop, tablet, and mobile devices
|
||||
- **Instant Updates**: Users always have the latest version
|
||||
- **Cockatrice Compatible**: Interoperates with existing Cockatrice users and protocol
|
||||
- **MTG Online Compatible**: Interoperates with existing MTG Online users and protocol
|
||||
- **Modern UX**: Contemporary interface design with modern web technologies
|
||||
|
||||
## Core Objectives
|
||||
@@ -27,7 +27,7 @@ A Progressive Web Application (PWA) that provides:
|
||||
### 1. User Experience
|
||||
- Intuitive, modern interface that rivals native desktop applications
|
||||
- Real-time multiplayer gameplay with minimal latency
|
||||
- Seamless deck building with import/export from Cockatrice
|
||||
- Seamless deck building with import/export from MTG Online
|
||||
- Responsive design that works across all screen sizes
|
||||
|
||||
### 2. Technical Excellence
|
||||
@@ -35,7 +35,7 @@ A Progressive Web Application (PWA) that provides:
|
||||
- **Frontend**: React + TypeScript with Zustand state management
|
||||
- **Database**: PostgreSQL with async SQLAlchemy ORM
|
||||
- **Real-time**: WebSocket-based game server for live multiplayer
|
||||
- **Protocol**: Full compatibility with Cockatrice protocol buffer messages
|
||||
- **Protocol**: Full compatibility with MTG Online protocol buffer messages
|
||||
|
||||
### 3. Feature Parity with Desktop
|
||||
- User authentication and account management
|
||||
@@ -68,16 +68,16 @@ A Progressive Web Application (PWA) that provides:
|
||||
### Secondary Users
|
||||
1. **Tournament Organizers**: Need reliable, accessible platform for events
|
||||
2. **Community Builders**: Want to create and manage playgroups
|
||||
3. **Developers**: Want to integrate with or extend the Cockatrice ecosystem
|
||||
3. **Developers**: Want to integrate with or extend the MTG Online ecosystem
|
||||
|
||||
### Existing Cockatrice Users
|
||||
### Existing MTG Online Users
|
||||
- Seamless migration path
|
||||
- Protocol compatibility for cross-play
|
||||
- Familiar deck formats and card data
|
||||
|
||||
## Key Differentiators
|
||||
|
||||
| Feature | Desktop Cockatrice | Cockatrice Web |
|
||||
| Feature | Desktop MTG Online | MTG Online Web |
|
||||
|---------|-------------------|----------------|
|
||||
| Installation | Required | None (browser-based) |
|
||||
| Platform | Desktop only | Any device with browser |
|
||||
@@ -95,7 +95,7 @@ A Progressive Web Application (PWA) that provides:
|
||||
- [ ] Users can join and play multiplayer games in real-time
|
||||
- [ ] Game state syncs correctly across all connected players
|
||||
- [ ] Admin users can manage accounts and moderate games
|
||||
- [ ] Deck formats are compatible with Cockatrice desktop client
|
||||
- [ ] Deck formats are compatible with MTG Online desktop client
|
||||
|
||||
### Performance Requirements
|
||||
- [ ] 95% of API responses < 100ms
|
||||
@@ -115,7 +115,7 @@ A Progressive Web Application (PWA) that provides:
|
||||
- [ ] Open-source with community contributions
|
||||
- [ ] Documentation for users and developers
|
||||
- [ ] Deployment guides for self-hosting
|
||||
- [ ] Integration with existing Cockatrice ecosystem
|
||||
- [ ] Integration with existing MTG Online ecosystem
|
||||
|
||||
## Technology Choices
|
||||
|
||||
@@ -160,7 +160,7 @@ A Progressive Web Application (PWA) that provides:
|
||||
|
||||
### Protocol
|
||||
**Why Protocol Buffer Compatibility?**
|
||||
- Interoperability with existing Cockatrice users
|
||||
- Interoperability with existing MTG Online users
|
||||
- Leverage existing card database and deck formats
|
||||
- Community adoption pathway
|
||||
- Battle-tested message format
|
||||
@@ -195,7 +195,7 @@ A Progressive Web Application (PWA) that provides:
|
||||
- Mitigation: Alembic migrations, backward compatibility
|
||||
|
||||
2. **Protocol Changes**
|
||||
- Risk: Cockatrice protocol evolves
|
||||
- Risk: MTG Online protocol evolves
|
||||
- Mitigation: Version support, backward compatibility
|
||||
|
||||
## Future Enhancements
|
||||
@@ -231,8 +231,8 @@ We commit to:
|
||||
## Contact
|
||||
|
||||
For questions, contributions, or support:
|
||||
- GitHub: https://github.com/cockatrice-web
|
||||
- Documentation: https://cockatrice-web.github.io/docs
|
||||
- GitHub: https://github.com/mtgonline-web
|
||||
- Documentation: https://mtgonline-web.github.io/docs
|
||||
- Discord: [Community server link]
|
||||
|
||||
## Version
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Backend System Test Prompt
|
||||
|
||||
## Project Context
|
||||
You are working on the **mtgonline** backend project located at `/home/wall-o/projects/mtgonline/backend/`.
|
||||
|
||||
The backend is a FastAPI application with:
|
||||
- PostgreSQL database (MTG Online + MTG data)
|
||||
- Redis caching
|
||||
- JWT authentication
|
||||
- MTG card search and statistics endpoints
|
||||
|
||||
## Current State
|
||||
Read the `state.json` file at `/home/wall-o/projects/mtgonline/backend/state.json` to understand:
|
||||
- What has been completed
|
||||
- What the current focus is
|
||||
- Any blockers or pending tasks
|
||||
|
||||
## Task: Full Backend System Test
|
||||
|
||||
Your goal is to perform a comprehensive system test of the backend to verify all components work correctly together.
|
||||
|
||||
### Steps to Follow:
|
||||
|
||||
1. **Read State File**
|
||||
- Read `/home/wall-o/projects/mtgonline/backend/state.json`
|
||||
- Understand current progress and what's been tested
|
||||
|
||||
2. **Check Docker Containers**
|
||||
- Run: `cd /home/wall-o/projects/mtgonline && docker compose ps`
|
||||
- If containers aren't running, start them: `docker compose up -d`
|
||||
- Wait for services to be healthy (~40 seconds)
|
||||
|
||||
3. **Run System Test**
|
||||
- Execute: `cd /home/wall-o/projects/mtgonline/backend && python test_system.py`
|
||||
- This tests:
|
||||
- Database connections (PostgreSQL + Redis)
|
||||
- Cache operations
|
||||
- Database tables
|
||||
- API endpoints (health, card search, statistics, auth)
|
||||
|
||||
4. **Analyze Results**
|
||||
- If tests fail, investigate and fix issues
|
||||
- Common issues:
|
||||
- Database not running
|
||||
- Redis connection failed
|
||||
- API routes not registered
|
||||
- Environment variables not set
|
||||
|
||||
5. **Update State**
|
||||
- Update `state.json` with test results
|
||||
- Note any bugs found and fixed
|
||||
- Document what's working and what needs attention
|
||||
|
||||
6. **Report Findings**
|
||||
- Summarize test results (pass/fail rates)
|
||||
- List any issues found
|
||||
- Recommend next steps
|
||||
|
||||
## Expected Test Coverage
|
||||
|
||||
The system test (`test_system.py`) should verify:
|
||||
- ✅ MTG Online PostgreSQL connection
|
||||
- ✅ MTG PostgreSQL connection
|
||||
- ✅ Redis connection and operations
|
||||
- ✅ MTG Online database tables (users, decks)
|
||||
- ✅ MTG database tables (sets, cards)
|
||||
- ✅ MTG card search functionality
|
||||
- ✅ Health endpoint
|
||||
- ✅ Card search API endpoint
|
||||
- ✅ Statistics API endpoint
|
||||
- ✅ Authentication endpoint
|
||||
|
||||
## Success Criteria
|
||||
|
||||
All tests should pass (100% success rate) before considering the backend ready for frontend development.
|
||||
|
||||
## Commands Reference
|
||||
|
||||
```bash
|
||||
# Check Docker status
|
||||
cd /home/wall-o/projects/mtgonline && docker compose ps
|
||||
|
||||
# Start containers
|
||||
cd /home/wall-o/projects/mtgonline && docker compose up -d
|
||||
|
||||
# Wait for health (40 seconds)
|
||||
sleep 40
|
||||
|
||||
# Run system test
|
||||
cd /home/wall-o/projects/mtgonline/backend && python test_system.py
|
||||
|
||||
# Check logs if issues
|
||||
cd /home/wall-o/projects/mtgonline && docker compose logs backend
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- All code execution must be as user `wall-o` (not root)
|
||||
- Use sudo -u wall-o when running commands
|
||||
- Pin versions in requirements.txt
|
||||
- Follow PEP 8 for Python code
|
||||
- Log errors with context
|
||||
- One change per commit, descriptive messages
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Cockatrice Web Application
|
||||
MTG Online Web Application
|
||||
|
||||
A modern web-based implementation of the Cockatrice multiplayer Magic: The Gathering platform.
|
||||
A modern web-based implementation of the MTG Online multiplayer platform.
|
||||
Built with FastAPI, WebSocket, and protocol buffer compatibility.
|
||||
|
||||
## Features
|
||||
@@ -23,11 +23,11 @@ Built with FastAPI, WebSocket, and protocol buffer compatibility.
|
||||
- Database: PostgreSQL with async driver
|
||||
- Authentication: JWT tokens with bcrypt password hashing
|
||||
- Game Server: WebSocket-based real-time multiplayer
|
||||
- Protocol: Compatible with Cockatrice protocol buffer messages
|
||||
- Protocol: Compatible with MTG Online protocol buffer messages
|
||||
|
||||
## License
|
||||
MIT License
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__author__ = "Cockatrice Web Team"
|
||||
__author__ = "MTG Online Web Team"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Core package
|
||||
@@ -2,7 +2,7 @@
|
||||
Database engine and session management.
|
||||
|
||||
Provides async SQLAlchemy engine and session factory for dependency injection.
|
||||
Supports dual database connections for cockatrice app and mtgjson data.
|
||||
Supports dual database connections for mtgonline app and mtgjson data.
|
||||
"""
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
@@ -10,7 +10,7 @@ from app.core.settings import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Primary database engine (cockatrice app)
|
||||
# Primary database engine (mtgonline app)
|
||||
engine = create_async_engine(
|
||||
settings.DATABASE_URL,
|
||||
echo=settings.DEBUG,
|
||||
@@ -50,7 +50,7 @@ __all__ = ["Base", "get_db", "async_session", "engine", "mtg_get_db", "mtg_async
|
||||
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
"""FastAPI dependency that provides a database session for the cockatrice app."""
|
||||
"""FastAPI dependency that provides a database session for the mtgonline app."""
|
||||
async with async_session() as session:
|
||||
try:
|
||||
yield session
|
||||
|
||||
@@ -12,17 +12,17 @@ class Settings(BaseSettings):
|
||||
"""Application settings loaded from environment variables or .env file."""
|
||||
|
||||
# Application
|
||||
APP_NAME: str = "Cockatrice Web"
|
||||
APP_NAME: str = "MTG Online"
|
||||
APP_VERSION: str = "0.2.0"
|
||||
DEBUG: bool = False
|
||||
SECRET_KEY: str = "change-me-in-production"
|
||||
JWT_SECRET_KEY: str = "change-me-in-production"
|
||||
|
||||
# Database - Primary (cockatrice app)
|
||||
DATABASE_URL: str = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/cockatrice"
|
||||
# Database - Primary (mtgonline app)
|
||||
DATABASE_URL: str = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgonline"
|
||||
|
||||
# Database - Secondary (mtgjson data)
|
||||
MTG_DATABASE_URL: str = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/mtgdata"
|
||||
MTG_DATABASE_URL: str = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgdata"
|
||||
|
||||
# Redis
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
@@ -55,12 +55,15 @@ class Settings(BaseSettings):
|
||||
# Database configuration
|
||||
DB_CONFIG: dict = {
|
||||
"engine": "postgresql+asyncpg",
|
||||
"user": "cockatrice",
|
||||
"password": "cockatrice_pass",
|
||||
"user": "mtgonline",
|
||||
"password": "mtgonline_pass",
|
||||
"host": "postgres",
|
||||
"port": 5432,
|
||||
}
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL: str = "INFO"
|
||||
|
||||
# Redis configuration
|
||||
REDIS_CONFIG: dict = {
|
||||
"host": "redis",
|
||||
|
||||
+104
-3
@@ -3,14 +3,27 @@ FastAPI application factory and middleware setup.
|
||||
|
||||
Configures CORS, authentication, and error handling.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text
|
||||
from app.core.settings import get_settings
|
||||
from app.routers import auth, users, decks, rooms, games, admin, card_router
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=getattr(logging, settings.LOG_LEVEL))
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
@@ -27,6 +40,68 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Track initialization status
|
||||
mtg_data_ready = False
|
||||
mtg_data_count = 0
|
||||
|
||||
|
||||
async def check_mtg_data_count() -> int:
|
||||
"""Check if MTG data has been loaded."""
|
||||
try:
|
||||
engine = create_async_engine(settings.MTG_DATABASE_URL)
|
||||
async with AsyncSession(engine) as session:
|
||||
stmt = text("SELECT COUNT(*) FROM mtg_cards")
|
||||
result = await session.execute(stmt)
|
||||
count = result.scalar()
|
||||
await engine.dispose()
|
||||
return count or 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking MTG data count: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
async def trigger_initial_mtg_download():
|
||||
"""Trigger initial MTGJSON download on first startup."""
|
||||
global mtg_data_ready, mtg_data_count
|
||||
|
||||
logger.info("Checking MTG data status...")
|
||||
count = await check_mtg_data_count()
|
||||
|
||||
if count == 0:
|
||||
logger.info("No MTG data found. Triggering initial download...")
|
||||
mtg_data_ready = False
|
||||
|
||||
try:
|
||||
# Import and run the refresh script
|
||||
from app.scripts.refresh_mtg import main as refresh_main
|
||||
await refresh_main()
|
||||
|
||||
# Re-check count after download
|
||||
mtg_data_count = await check_mtg_data_count()
|
||||
mtg_data_ready = mtg_data_count > 0
|
||||
|
||||
if mtg_data_ready:
|
||||
logger.info(f"MTG data loaded successfully: {mtg_data_count} cards")
|
||||
else:
|
||||
logger.warning("MTG data download completed but no cards found")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load MTG data: {e}")
|
||||
mtg_data_ready = False
|
||||
else:
|
||||
logger.info(f"MTG data already loaded: {count} cards")
|
||||
mtg_data_ready = True
|
||||
mtg_data_count = count
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Run initial MTG data download on startup."""
|
||||
logger.info("Starting MTG Online backend...")
|
||||
|
||||
# Run MTG data download in background to not block startup
|
||||
asyncio.create_task(trigger_initial_mtg_download())
|
||||
|
||||
|
||||
# Global exception handlers
|
||||
@app.exception_handler(Exception)
|
||||
@@ -49,6 +124,32 @@ app.include_router(admin.router, prefix="/api/v1/admin", tags=["Admin"])
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint for monitoring."""
|
||||
return {"status": "healthy", "version": settings.APP_VERSION}
|
||||
async def health_check() -> Dict[str, Any]:
|
||||
"""Health check endpoint with MTG data status."""
|
||||
health_status = {
|
||||
"status": "healthy",
|
||||
"version": settings.APP_VERSION,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
# Check database connectivity
|
||||
try:
|
||||
engine = create_async_engine(settings.DATABASE_URL)
|
||||
async with AsyncSession(engine) as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
await engine.dispose()
|
||||
health_status["database"] = "connected"
|
||||
except Exception as e:
|
||||
health_status["status"] = "degraded"
|
||||
health_status["database"] = f"error: {str(e)}"
|
||||
|
||||
# Check MTG data status
|
||||
health_status["mtg_data"] = {
|
||||
"ready": mtg_data_ready,
|
||||
"count": mtg_data_count,
|
||||
}
|
||||
|
||||
if not mtg_data_ready:
|
||||
health_status["status"] = "initializing"
|
||||
|
||||
return health_status
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Models package
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
SQLAlchemy ORM models for the Cockatrice database.
|
||||
SQLAlchemy ORM models for the MTG Online database.
|
||||
|
||||
Mirrors the original MySQL schema with modern PostgreSQL features.
|
||||
"""
|
||||
@@ -11,7 +11,7 @@ from app.core.database import Base
|
||||
|
||||
class User(Base):
|
||||
"""User account model."""
|
||||
__tablename__ = "cockatrice_users"
|
||||
__tablename__ = "mtgonline_users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(64), unique=True, nullable=False, index=True)
|
||||
@@ -47,12 +47,12 @@ class User(Base):
|
||||
|
||||
class DecklistFolder(Base):
|
||||
"""User deck folder."""
|
||||
__tablename__ = "cockatrice_decklist_folders"
|
||||
__tablename__ = "mtgonline_decklist_folders"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
owner_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
|
||||
owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
||||
name = Column(String(255), nullable=False)
|
||||
parent_id = Column(Integer, ForeignKey("cockatrice_decklist_folders.id"), nullable=True)
|
||||
parent_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True)
|
||||
creation_date = Column(DateTime, server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
@@ -64,11 +64,11 @@ class DecklistFolder(Base):
|
||||
|
||||
class DecklistFile(Base):
|
||||
"""Deck file stored in a folder."""
|
||||
__tablename__ = "cockatrice_decklist_files"
|
||||
__tablename__ = "mtgonline_decklist_files"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
folder_id = Column(Integer, ForeignKey("cockatrice_decklist_folders.id"), nullable=True)
|
||||
owner_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
|
||||
folder_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True)
|
||||
owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
||||
name = Column(String(255), nullable=False)
|
||||
content = Column(Text, nullable=False) # Native XML or plain text deck format
|
||||
format = Column(String(50), default="native") # 'native' or 'plain'
|
||||
@@ -84,7 +84,7 @@ class DecklistFile(Base):
|
||||
|
||||
class Room(Base):
|
||||
"""Chat room."""
|
||||
__tablename__ = "cockatrice_rooms"
|
||||
__tablename__ = "mtgonline_rooms"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(100), unique=True, nullable=False)
|
||||
@@ -99,10 +99,10 @@ class Room(Base):
|
||||
|
||||
class RoomGameType(Base):
|
||||
"""Game type definition for a room."""
|
||||
__tablename__ = "cockatrice_rooms_gametypes"
|
||||
__tablename__ = "mtgonline_rooms_gametypes"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
room_id = Column(Integer, ForeignKey("cockatrice_rooms.id"), nullable=False)
|
||||
room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=False)
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
@@ -112,10 +112,10 @@ class RoomGameType(Base):
|
||||
|
||||
class Ban(Base):
|
||||
"""User ban record."""
|
||||
__tablename__ = "cockatrice_bans"
|
||||
__tablename__ = "mtgonline_bans"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
||||
server_id = Column(Integer, nullable=True)
|
||||
reason = Column(Text, nullable=False)
|
||||
moderators = Column(String(255), nullable=True) # Admin usernames
|
||||
@@ -133,11 +133,11 @@ class Ban(Base):
|
||||
|
||||
class GameLog(Base):
|
||||
"""Game log entry."""
|
||||
__tablename__ = "cockatrice_log"
|
||||
__tablename__ = "mtgonline_log"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
room_id = Column(Integer, ForeignKey("cockatrice_rooms.id"), nullable=True)
|
||||
player_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=True)
|
||||
room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=True)
|
||||
player_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True)
|
||||
message = Column(Text, nullable=False)
|
||||
timestamp = Column(DateTime, server_default=func.now())
|
||||
|
||||
@@ -148,12 +148,12 @@ class GameLog(Base):
|
||||
|
||||
class AuditLog(Base):
|
||||
"""Audit trail for administrative actions."""
|
||||
__tablename__ = "cockatrice_audit"
|
||||
__tablename__ = "mtgonline_audit"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
admin_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=False)
|
||||
admin_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
||||
action_type = Column(String(50), nullable=False) # 'ban', 'unban', 'warn', etc.
|
||||
target_user_id = Column(Integer, ForeignKey("cockatrice_users.id"), nullable=True)
|
||||
target_user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True)
|
||||
details = Column(Text, nullable=True)
|
||||
ip_address = Column(String(45), nullable=True)
|
||||
timestamp = Column(DateTime, server_default=func.now())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
MTG Database Monitor
|
||||
|
||||
Monitors PostgreSQL database metrics for both cockatrice and mtgdata databases.
|
||||
Monitors PostgreSQL database metrics for both mtgonline and mtgdata databases.
|
||||
Tracks:
|
||||
- Database size and growth
|
||||
- Table sizes
|
||||
@@ -28,14 +28,14 @@ MTG_DB = settings.MTG_DATABASE_URL
|
||||
class MtgMonitor:
|
||||
def __init__(self):
|
||||
self.mtg_conn = None
|
||||
self.cockatrice_conn = None
|
||||
self.mtgonline_conn = None
|
||||
self.metrics = {}
|
||||
|
||||
async def connect(self):
|
||||
"""Establish connections to both databases."""
|
||||
try:
|
||||
self.mtg_conn = await asyncpg.connect(MTG_DB)
|
||||
self.cockatrice_conn = await asyncpg.connect(COCKATRICE_DB)
|
||||
self.mtgonline_conn = await asyncpg.connect(COCKATRICE_DB)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Connection error: {e}")
|
||||
@@ -45,8 +45,8 @@ class MtgMonitor:
|
||||
"""Close database connections."""
|
||||
if self.mtg_conn:
|
||||
await self.mtg_conn.close()
|
||||
if self.cockatrice_conn:
|
||||
await self.cockatrice_conn.close()
|
||||
if self.mtgonline_conn:
|
||||
await self.mtgonline_conn.close()
|
||||
|
||||
async def get_database_size(self) -> Dict[str, float]:
|
||||
"""Get size of databases in GB."""
|
||||
@@ -56,14 +56,14 @@ class MtgMonitor:
|
||||
SELECT pg_database_size(current_database()) as size
|
||||
""")
|
||||
|
||||
# Cockatrice database size
|
||||
cockatrice_size = await self.cockatrice_conn.fetchval("""
|
||||
# MTG Online database size
|
||||
mtgonline_size = await self.mtgonline_conn.fetchval("""
|
||||
SELECT pg_database_size(current_database()) as size
|
||||
""")
|
||||
|
||||
return {
|
||||
"mtgdata": mtg_size / (1024**3), # Convert to GB
|
||||
"cockatrice": cockatrice_size / (1024**3)
|
||||
"mtgonline": mtgonline_size / (1024**3)
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error getting database sizes: {e}")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Routers package
|
||||
@@ -21,7 +21,7 @@ async def list_games(
|
||||
):
|
||||
"""List active games."""
|
||||
# This would query a games table - simplified for now
|
||||
# In production, you'd have a CockatriceGames model
|
||||
# In production, you'd have a MTG OnlineGames model
|
||||
return []
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Games package
|
||||
@@ -0,0 +1 @@
|
||||
# Schemas package
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Cockatrice protocol constants and message definitions."""
|
||||
"""MTG Online protocol constants and message definitions."""
|
||||
import enum
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ class CardInfo:
|
||||
|
||||
|
||||
class DeckParser:
|
||||
"""Parse Cockatrice deck list formats."""
|
||||
"""Parse MTG Online deck list formats."""
|
||||
|
||||
# Regex patterns for deck parsing
|
||||
CARD_LINE_RE = re.compile(r"^\s*[\w\[\(\{].*$")
|
||||
@@ -149,8 +149,8 @@ class DeckParser:
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_native_xml(self, cards: List[CardInfo]) -> str:
|
||||
"""Convert cards to Cockatrice native XML format."""
|
||||
xml_lines = ['<cockatrice_deck version="1">']
|
||||
"""Convert cards to MTG Online native XML format."""
|
||||
xml_lines = ['<mtgonline_deck version="1">']
|
||||
|
||||
# Group cards by zone (main/sideboard)
|
||||
main_cards = [c for c in cards if not c.name.startswith("[SB]")]
|
||||
@@ -169,11 +169,11 @@ class DeckParser:
|
||||
xml_lines.append(f' <card name="{clean_name}" count="{card.count}" />')
|
||||
xml_lines.append(' </zone>')
|
||||
|
||||
xml_lines.append('</cockatrice_deck>')
|
||||
xml_lines.append('</mtgonline_deck>')
|
||||
return "\n".join(xml_lines)
|
||||
|
||||
def from_native_xml(self, xml: str) -> List[CardInfo]:
|
||||
"""Parse Cockatrice native XML format."""
|
||||
"""Parse MTG Online native XML format."""
|
||||
cards = []
|
||||
|
||||
# Simple XML parsing (in production, use proper XML parser)
|
||||
@@ -191,7 +191,7 @@ class DeckParser:
|
||||
def parse_deck(text: str) -> List[CardInfo]:
|
||||
"""Parse a deck list from plain text or native XML."""
|
||||
parser = DeckParser()
|
||||
if text.strip().startswith("<cockatrice_deck"):
|
||||
if text.strip().startswith("<mtgonline_deck"):
|
||||
return parser.from_native_xml(text)
|
||||
else:
|
||||
return parser.parse_plain_text(text)
|
||||
|
||||
@@ -239,7 +239,7 @@ async def game_websocket_endpoint(websocket: WebSocket, game_id: int):
|
||||
await room.send_to_player(player_id, {
|
||||
"type": "pong",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}))
|
||||
})
|
||||
except WebSocketDisconnect:
|
||||
# Player disconnected
|
||||
await room.remove_player(player_id)
|
||||
@@ -297,7 +297,7 @@ async def process_game_command(room: GameRoom, player_id: int, command: dict):
|
||||
await room.send_to_player(player_id, {
|
||||
"type": "error",
|
||||
"message": f"Invalid command type: {cmd_type}",
|
||||
}))
|
||||
})
|
||||
return
|
||||
|
||||
# Broadcast command as game event
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
Database initialization script for MTG Online backend.
|
||||
|
||||
Creates all necessary tables matching the SQLAlchemy ORM models
|
||||
for both databases.
|
||||
"""
|
||||
import asyncio
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text
|
||||
from app.core.settings import get_settings
|
||||
|
||||
|
||||
async def setup_mtg_online_database():
|
||||
"""Create tables for the mtgonline database."""
|
||||
settings = get_settings()
|
||||
engine = create_async_engine(settings.MTGO_DATABASE_URL)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
# Create tables
|
||||
tables = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(64) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(128) NOT NULL,
|
||||
salt VARCHAR(128) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
country VARCHAR(2),
|
||||
real_name VARCHAR(128),
|
||||
avatar_bmp TEXT,
|
||||
privlevel VARCHAR(50) DEFAULT 'User',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_banned BOOLEAN DEFAULT FALSE,
|
||||
ban_reason TEXT,
|
||||
ban_ends TIMESTAMP,
|
||||
vip_status INTEGER DEFAULT 0,
|
||||
vip_expiry TIMESTAMP,
|
||||
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_decklist_folders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
owner_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
parent_id INTEGER REFERENCES mtgonline_decklist_folders(id) ON DELETE CASCADE,
|
||||
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_decklist_files (
|
||||
id SERIAL PRIMARY KEY,
|
||||
folder_id INTEGER REFERENCES mtgonline_decklist_folders(id) ON DELETE CASCADE,
|
||||
owner_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
format VARCHAR(50) DEFAULT 'native',
|
||||
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_rooms (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
is_password_protected BOOLEAN DEFAULT FALSE,
|
||||
password_hash VARCHAR(128),
|
||||
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_rooms_gametypes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
room_id INTEGER REFERENCES mtgonline_rooms(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_bans (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
|
||||
server_id INTEGER,
|
||||
reason TEXT NOT NULL,
|
||||
moderators VARCHAR(255),
|
||||
ip_address VARCHAR(45),
|
||||
expiration_time TIMESTAMP,
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
creation_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
room_id INTEGER REFERENCES mtgonline_rooms(id) ON DELETE CASCADE,
|
||||
player_id INTEGER REFERENCES mtgonline_users(id) ON DELETE CASCADE,
|
||||
message TEXT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtgonline_audit (
|
||||
id SERIAL PRIMARY KEY,
|
||||
admin_id INTEGER REFERENCES mtgonline_users(id) ON DELETE SET NULL,
|
||||
action_type VARCHAR(50) NOT NULL,
|
||||
target_user_id INTEGER REFERENCES mtgonline_users(id) ON DELETE SET NULL,
|
||||
details TEXT,
|
||||
ip_address VARCHAR(45),
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
]
|
||||
|
||||
for table_sql in tables:
|
||||
await session.execute(text(table_sql))
|
||||
|
||||
# Create indexes
|
||||
indexes = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_decks_owner ON mtgonline_decklist_files(owner_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_decks_folder ON mtgonline_decklist_files(folder_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_bans_active ON mtgonline_bans(active);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_log_timestamp ON mtgonline_log(timestamp);",
|
||||
]
|
||||
|
||||
for idx_sql in indexes:
|
||||
await session.execute(text(idx_sql))
|
||||
|
||||
print("✓ All mtgonline tables created")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def setup_mtg_data_database():
|
||||
"""Create tables for the mtgdata database."""
|
||||
settings = get_settings()
|
||||
engine = create_async_engine(settings.MTG_DATABASE_URL)
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
# Create tables
|
||||
tables = [
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtg_sets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
code VARCHAR(10) UNIQUE NOT NULL,
|
||||
name VARCHAR(255),
|
||||
type VARCHAR(100),
|
||||
release_date DATE,
|
||||
base_set_size INTEGER,
|
||||
total_size INTEGER,
|
||||
is_foil_only BOOLEAN,
|
||||
is_non_foil_only BOOLEAN,
|
||||
digital BOOLEAN,
|
||||
icon_svg_url TEXT,
|
||||
parent_code VARCHAR(10),
|
||||
mtgo_code VARCHAR(10),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mtg_cards (
|
||||
id SERIAL PRIMARY KEY,
|
||||
set_id INTEGER REFERENCES mtg_sets(id) ON DELETE CASCADE,
|
||||
name VARCHAR(255),
|
||||
mana_cost VARCHAR(255),
|
||||
type_line VARCHAR(255),
|
||||
oracle_text TEXT,
|
||||
power VARCHAR(50),
|
||||
toughness VARCHAR(50),
|
||||
rarity VARCHAR(50),
|
||||
layout VARCHAR(50),
|
||||
artist VARCHAR(255),
|
||||
flavor_text TEXT,
|
||||
numbers VARCHAR(100),
|
||||
identifiers TEXT,
|
||||
images TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
]
|
||||
|
||||
for table_sql in tables:
|
||||
await session.execute(text(table_sql))
|
||||
|
||||
# Create indexes
|
||||
indexes = [
|
||||
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_name ON mtg_cards(name);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_type ON mtg_cards(type_line);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_rarity ON mtg_cards(rarity);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_mtg_cards_set_id ON mtg_cards(set_id);",
|
||||
]
|
||||
|
||||
for idx_sql in indexes:
|
||||
await session.execute(text(idx_sql))
|
||||
|
||||
print("✓ All mtgdata tables created")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main initialization function."""
|
||||
print("MTG Online Database Initialization")
|
||||
print("=" * 50)
|
||||
|
||||
# Setup mtgonline database
|
||||
print("\nSetting up mtgonline database...")
|
||||
await setup_mtg_online_database()
|
||||
|
||||
# Setup mtgdata database
|
||||
print("\nSetting up mtgdata database...")
|
||||
await setup_mtg_data_database()
|
||||
|
||||
print("\n✓ Database initialization complete!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+26
-16
@@ -3,7 +3,7 @@
|
||||
Backend System Test Script
|
||||
|
||||
Tests all backend components:
|
||||
- Database connections (Cockatrice + MTG)
|
||||
- Database connections (MTG Online + MTG)
|
||||
- Redis connection and caching
|
||||
- Card database queries
|
||||
- API endpoints
|
||||
@@ -21,10 +21,20 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy import text
|
||||
|
||||
# Database URLs
|
||||
COCKATRICE_DB = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/cockatrice"
|
||||
MTG_DB = "postgresql+asyncpg://cockatrice:cockatrice_pass@localhost:5432/mtgdata"
|
||||
COCKATRICE_DB = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgonline"
|
||||
MTG_DB = "postgresql+asyncpg://mtgonline:mtgonline_pass@localhost:5432/mtgdata"
|
||||
REDIS_URL = "redis://localhost:6379/0"
|
||||
|
||||
# Test URLs - detect environment
|
||||
import socket
|
||||
try:
|
||||
# Try Docker network first
|
||||
socket.getaddrinfo('mtg_backend', 8000)
|
||||
API_BASE = 'http://mtg_backend:8000'
|
||||
except:
|
||||
# Fall back to host port
|
||||
API_BASE = 'http://localhost:8001'
|
||||
print(f"Using API base: {API_BASE}")
|
||||
results = {
|
||||
"tests_run": 0,
|
||||
"tests_passed": 0,
|
||||
@@ -97,29 +107,29 @@ async def test_cache_operations() -> bool:
|
||||
print(f"✗ Redis cache operations: FAILED - {str(e)}")
|
||||
return False
|
||||
|
||||
async def test_cockatrice_tables() -> bool:
|
||||
"""Test Cockatrice database tables."""
|
||||
async def test_mtgonline_tables() -> bool:
|
||||
"""Test MTG Online database tables."""
|
||||
results["tests_run"] += 1
|
||||
try:
|
||||
engine = create_async_engine(COCKATRICE_DB, echo=False)
|
||||
async with engine.connect() as conn:
|
||||
# Test users table
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM cockatrice_users"))
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM mtgonline_users"))
|
||||
user_count = result.scalar()
|
||||
|
||||
# Test decks table
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM cockatrice_decklist_files"))
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM mtgonline_decklist_files"))
|
||||
deck_count = result.scalar()
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
results["tests_passed"] += 1
|
||||
print(f"✓ Cockatrice tables: OK (users: {user_count}, decks: {deck_count})")
|
||||
print(f"✓ MTG Online tables: OK (users: {user_count}, decks: {deck_count})")
|
||||
return True
|
||||
except Exception as e:
|
||||
results["tests_failed"] += 1
|
||||
results["failures"].append(f"Cockatrice tables: {str(e)}")
|
||||
print(f"✗ Cockatrice tables: FAILED - {str(e)}")
|
||||
results["failures"].append(f"MTG Online tables: {str(e)}")
|
||||
print(f"✗ MTG Online tables: FAILED - {str(e)}")
|
||||
return False
|
||||
|
||||
async def test_mtg_tables() -> bool:
|
||||
@@ -181,7 +191,7 @@ async def test_health_endpoint() -> bool:
|
||||
results["tests_run"] += 1
|
||||
try:
|
||||
import requests
|
||||
response = requests.get("http://localhost:8000/health", timeout=5)
|
||||
response = requests.get("http://localhost:8001/health", timeout=5)
|
||||
|
||||
if response.status_code == 200:
|
||||
results["tests_passed"] += 1
|
||||
@@ -204,7 +214,7 @@ async def test_card_search_endpoint() -> bool:
|
||||
try:
|
||||
import requests
|
||||
response = requests.get(
|
||||
"http://localhost:8000/mtg/cards/search",
|
||||
"http://localhost:8001/api/v1/mtg/cards/search",
|
||||
params={"q": "Lightning Bolt", "limit": 5},
|
||||
timeout=10
|
||||
)
|
||||
@@ -231,7 +241,7 @@ async def test_statistics_endpoint() -> bool:
|
||||
try:
|
||||
import requests
|
||||
response = requests.get(
|
||||
"http://localhost:8000/mtg/cards/statistics",
|
||||
"http://localhost:8001/api/v1/mtg/cards/statistics",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
@@ -258,7 +268,7 @@ async def test_auth_endpoint() -> bool:
|
||||
try:
|
||||
import requests
|
||||
response = requests.post(
|
||||
"http://localhost:8000/auth/register",
|
||||
"http://localhost:8001/api/v1/auth/register",
|
||||
json={
|
||||
"username": "test_user",
|
||||
"email": "test@example.com",
|
||||
@@ -295,7 +305,7 @@ async def run_all_tests():
|
||||
# Database connections
|
||||
print("Testing Database Connections:")
|
||||
print("-" * 60)
|
||||
await test_connection(COCKATRICE_DB, "Cockatrice PostgreSQL")
|
||||
await test_connection(COCKATRICE_DB, "MTG Online PostgreSQL")
|
||||
await test_connection(MTG_DB, "MTG PostgreSQL")
|
||||
await test_redis()
|
||||
print()
|
||||
@@ -309,7 +319,7 @@ async def run_all_tests():
|
||||
# Database tables
|
||||
print("Testing Database Tables:")
|
||||
print("-" * 60)
|
||||
await test_cockatrice_tables()
|
||||
await test_mtgonline_tables()
|
||||
await test_mtg_tables()
|
||||
await test_mtg_card_query()
|
||||
print()
|
||||
|
||||
@@ -4,15 +4,15 @@ services:
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
environment:
|
||||
POSTGRES_DB: cockatrice
|
||||
POSTGRES_USER: cockatrice_user
|
||||
POSTGRES_PASSWORD: cockatrice_password
|
||||
POSTGRES_DB: mtgonline
|
||||
POSTGRES_USER: mtgonline_user
|
||||
POSTGRES_PASSWORD: mtgonline_password
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U cockatrice_user"]
|
||||
test: ["CMD-SHELL", "pg_isready -U mtgonline_user"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
+62
-14
@@ -1,15 +1,15 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
# PostgreSQL - hosts both Cockatrice app data and mtgjson data
|
||||
# PostgreSQL - MTG Online App Database
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: mtg_postgres
|
||||
container_name: mtgonline_postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-cockatrice}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-cockatrice_pass}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-cockatrice}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-mtgonline}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mtgonline_pass}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-mtgonline}
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:5432"
|
||||
@@ -19,31 +19,75 @@ services:
|
||||
networks:
|
||||
- mtg_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-cockatrice}"]
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mtgonline}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# PostgreSQL - MTG Data Database
|
||||
postgres-mtgdata:
|
||||
image: postgres:16-alpine
|
||||
container_name: mtgonline_postgres_mtgdata
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-mtgonline}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mtgonline_pass}
|
||||
POSTGRES_DB: ${MTGDATA_DB:-mtgdata}
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C"
|
||||
ports:
|
||||
- "${MTGDATA_PORT:-5433}:5432"
|
||||
volumes:
|
||||
- postgres_mtgdata_data:/var/lib/postgresql/data
|
||||
- ./scripts/init-mtgdata.sql:/docker-entrypoint-initdb.d/01-init.sql
|
||||
networks:
|
||||
- mtg_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mtgonline}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# Redis cache
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: mtgonline_redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${REDIS_PORT:-6379}:6379"
|
||||
networks:
|
||||
- mtg_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
|
||||
# Backend API
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: mtg_backend
|
||||
container_name: mtgonline_backend
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
postgres-mtgdata:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://cockatrice:cockatrice_pass@postgres:5432/cockatrice}
|
||||
- MTG_DATABASE_URL=${MTG_DATABASE_URL:-postgresql+asyncpg://cockatrice:cockatrice_pass@postgres:5432/mtgdata}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://mtgonline:mtgonline_pass@postgres:5432/mtgonline}
|
||||
- MTG_DATABASE_URL=${MTG_DATABASE_URL:-postgresql+asyncpg://mtgonline:mtgonline_pass@postgres-mtgdata:5432/mtgdata}
|
||||
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-change-me-in-production}
|
||||
- JWT_ALGORITHM=${JWT_ALGORITHM:-HS256}
|
||||
- ACCESS_TOKEN_EXPIRE_MINUTES=${ACCESS_TOKEN_EXPIRE_MINUTES:-60}
|
||||
- REFRESH_TOKEN_EXPIRE_DAYS=${REFRESH_TOKEN_EXPIRE_DAYS:-7}
|
||||
- CORS_ORIGINS=${CORS_ORIGINS:-["http://localhost:3000","http://localhost:8000"]}
|
||||
- APP_NAME=${APP_NAME:-Cockatrice Web}
|
||||
- APP_NAME=${APP_NAME:-MTG Online}
|
||||
- APP_VERSION=${APP_VERSION:-0.2.0}
|
||||
- DEBUG=${DEBUG:-False}
|
||||
- MTG_REFRESH_INTERVAL_DAYS=${MTG_REFRESH_INTERVAL_DAYS:-7}
|
||||
@@ -51,7 +95,7 @@ services:
|
||||
- UPLOAD_DIR=${UPLOAD_DIR:-/app/uploads}
|
||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||
ports:
|
||||
- "${BACKEND_PORT:-8000}:8000"
|
||||
- "${BACKEND_PORT:-8001}:8000"
|
||||
volumes:
|
||||
- mtg_data:/app/data
|
||||
- mtg_uploads:/app/uploads
|
||||
@@ -70,14 +114,16 @@ services:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: mtg_refresh
|
||||
container_name: mtgonline_refresh
|
||||
restart: "no"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
postgres-mtgdata:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://cockatrice:cockatrice_pass@postgres:5432/cockatrice}
|
||||
- MTG_DATABASE_URL=${MTG_DATABASE_URL:-postgresql+asyncpg://cockatrice:cockatrice_pass@postgres:5432/mtgdata}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql+asyncpg://mtgonline:mtgonline_pass@postgres:5432/mtgonline}
|
||||
- MTG_DATABASE_URL=${MTG_DATABASE_URL:-postgresql+asyncpg://mtgonline:mtgonline_pass@postgres-mtgdata:5432/mtgdata}
|
||||
- DATA_DIR=${DATA_DIR:-/app/data}
|
||||
- MTG_REFRESH_INTERVAL_DAYS=${MTG_REFRESH_INTERVAL_DAYS:-7}
|
||||
volumes:
|
||||
@@ -89,6 +135,8 @@ services:
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
postgres_mtgdata_data:
|
||||
driver: local
|
||||
mtg_data:
|
||||
driver: local
|
||||
mtg_uploads:
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
-- Initialize MTG data database
|
||||
CREATE DATABASE mtgdata;
|
||||
-- Initialize MTG Online app database
|
||||
-- Note: mtgdata database is created separately in init-mtgdata.sql
|
||||
|
||||
-- Create extensions
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
-- Initialize MTG data database
|
||||
-- Uses $POSTGRES_USER from environment
|
||||
|
||||
-- Create mtgdata database if it doesn't exist (owned by the POSTGRES_USER)
|
||||
SELECT 'CREATE DATABASE mtgdata OWNER ' || current_user
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'mtgdata')\gexec
|
||||
|
||||
-- Connect to mtgdata and set up schema permissions
|
||||
\c mtgdata
|
||||
|
||||
-- Grant all privileges to the app user
|
||||
GRANT ALL PRIVILEGES ON DATABASE mtgdata TO current_user;
|
||||
GRANT ALL ON SCHEMA public TO current_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO current_user;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO current_user;
|
||||
|
||||
-- Create extensions
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
|
||||
|
||||
-- Create mtgjson tables
|
||||
CREATE TABLE IF NOT EXISTS mtg_sets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
code VARCHAR(10) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50),
|
||||
release_date DATE,
|
||||
base_set_size INTEGER,
|
||||
total_size INTEGER,
|
||||
is_foil_only BOOLEAN DEFAULT FALSE,
|
||||
is_non_foil_only BOOLEAN DEFAULT FALSE,
|
||||
digital BOOLEAN DEFAULT FALSE,
|
||||
icon_svg_url TEXT,
|
||||
parent_code VARCHAR(10),
|
||||
mtgo_code VARCHAR(10),
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mtg_cards (
|
||||
id SERIAL PRIMARY KEY,
|
||||
set_id INTEGER REFERENCES mtg_sets(id),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
mana_cost TEXT,
|
||||
type_line VARCHAR(255),
|
||||
oracle_text TEXT,
|
||||
power VARCHAR(10),
|
||||
toughness VARCHAR(10),
|
||||
rarity VARCHAR(50),
|
||||
layout VARCHAR(50),
|
||||
artist VARCHAR(255),
|
||||
flavor_text TEXT,
|
||||
numbers VARCHAR(50),
|
||||
identifiers JSONB,
|
||||
images JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_mtg_cards_set_id ON mtg_cards(set_id);
|
||||
CREATE INDEX idx_mtg_cards_name ON mtg_cards(name);
|
||||
CREATE INDEX idx_mtg_cards_type ON mtg_cards(type_line);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mtg_refresh_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
refresh_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
cards_updated INTEGER DEFAULT 0,
|
||||
sets_updated INTEGER DEFAULT 0,
|
||||
error_message TEXT,
|
||||
duration_seconds INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX idx_mtg_refresh_log_date ON mtg_refresh_log(refresh_date);
|
||||
Reference in New Issue
Block a user