import os import json from pathlib import Path from datetime import datetime from contextlib import asynccontextmanager from typing import AsyncGenerator, Generator, Optional from sqlalchemy import ( create_engine, Column, Integer, String, Float, Boolean, DateTime, ForeignKey, Text, text, inspect, Index ) from sqlalchemy.orm import sessionmaker, relationship, DeclarativeBase from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker # ─── Config ────────────────────────────────────────────────────────────────── DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./cs2_simulator.db") ASYNC_DATABASE_URL = os.getenv("ASYNC_DATABASE_URL", "sqlite+aiosqlite:///./cs2_simulator.db") REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0") USE_ASYNC = os.getenv("USE_ASYNC", "0") == "1" or "postgresql" in ASYNC_DATABASE_URL class Base(DeclarativeBase): pass # ─── Sync engine (SQLite fallback / admin) ────────────────────────────────── is_sqlite = DATABASE_URL.startswith("sqlite") sync_connect_args = {"check_same_thread": False} if is_sqlite else {} engine = create_engine(DATABASE_URL, connect_args=sync_connect_args, pool_pre_ping=True) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) if is_sqlite: with engine.connect() as conn: conn.execute(text("PRAGMA journal_mode=WAL")) conn.execute(text("PRAGMA synchronous=NORMAL")) conn.execute(text("PRAGMA cache_size=-64000")) conn.execute(text("PRAGMA busy_timeout=5000")) conn.commit() # ─── Async engine (PostgreSQL production) ─────────────────────────────────── async_engine = create_async_engine( ASYNC_DATABASE_URL, pool_size=20, max_overflow=10, pool_pre_ping=True, echo=False, ) AsyncSessionLocal = async_sessionmaker(async_engine, expire_on_commit=False) # ─── Models ───────────────────────────────────────────────────────────────── class User(Base): __tablename__ = "users" __table_args__ = ( Index("ix_users_username", "username"), Index("ix_users_created_at", "created_at"), ) id = Column(Integer, primary_key=True) username = Column(String(50), unique=True, nullable=False) hashed_password = Column(String(200), nullable=False) balance = Column(Float, default=0.0) is_admin = Column(Boolean, default=False) is_banned = Column(Boolean, default=False) created_at = Column(DateTime, default=datetime.utcnow) last_login = Column(DateTime, nullable=True) card_balance_cached = Column(Float, default=1000.0) total_deposited = Column(Float, default=0.0) total_withdrawn = Column(Float, default=0.0) last_deposit_date = Column(DateTime, nullable=True) inventory_items = relationship("InventoryItem", back_populates="user", cascade="all, delete-orphan") case_openings = relationship("CaseOpening", back_populates="user", cascade="all, delete-orphan") contracts = relationship("Contract", back_populates="user", cascade="all, delete-orphan") rpu_settings = relationship("UserRPU", back_populates="user", uselist=False, cascade="all, delete-orphan") upgrades = relationship("Upgrade", back_populates="user", cascade="all, delete-orphan") upgrade_rpu = relationship("UpgradeRPU", back_populates="user", uselist=False, cascade="all, delete-orphan") transactions = relationship("TransactionLog", back_populates="user", cascade="all, delete-orphan") class UpgradeRPU(Base): __tablename__ = "upgrade_rpu" __table_args__ = (Index("ix_upgrade_rpu_user_id", "user_id", unique=True),) id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True) upgrade_multiplier = Column(Float, default=1.0) auto_adjust = Column(Boolean, default=False) total_attempts = Column(Integer, default=0) total_success = Column(Integer, default=0) total_spent_value = Column(Float, default=0.0) current_win_streak = Column(Integer, default=0) current_lose_streak = Column(Integer, default=0) best_win_streak = Column(Integer, default=0) worst_lose_streak = Column(Integer, default=0) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) user = relationship("User", back_populates="upgrade_rpu") class Upgrade(Base): __tablename__ = "upgrades" __table_args__ = (Index("ix_upgrades_user_id_created", "user_id", "created_at"),) id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) input_item_id = Column(Integer, nullable=False) input_item_name = Column(String(200)) input_item_image_url = Column(String(500), default="") target_item_id = Column(Integer, nullable=False) target_item_name = Column(String(200)) target_item_image_url = Column(String(500), default="") success = Column(Boolean, default=False) probability = Column(Float) rpu_adjusted_probability = Column(Float) created_at = Column(DateTime, default=datetime.utcnow) user = relationship("User", back_populates="upgrades") class UserRPU(Base): __tablename__ = "user_rpu" __table_args__ = (Index("ix_user_rpu_user_id", "user_id", unique=True),) id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False, unique=True) consumer_multiplier = Column(Float, default=1.0) industrial_multiplier = Column(Float, default=1.0) mil_spec_multiplier = Column(Float, default=1.0) restricted_multiplier = Column(Float, default=1.0) classified_multiplier = Column(Float, default=1.0) covert_multiplier = Column(Float, default=1.0) rare_special_multiplier = Column(Float, default=1.0) luck_multiplier = Column(Float, default=1.0) auto_adjust = Column(Boolean, default=False) total_spent = Column(Float, default=0.0) total_opened = Column(Integer, default=0) total_value_received = Column(Float, default=0.0) last_adjustment = Column(DateTime, nullable=True) current_streak = Column(Integer, default=0) best_streak = Column(Integer, default=0) worst_streak = Column(Integer, default=0) last_results = Column(String(500), default="") luck_budget = Column(Float, default=100.0) session_spent = Column(Float, default=0.0) session_won = Column(Float, default=0.0) session_reset_date = Column(DateTime, nullable=True) ceiling_multiplier = Column(Float, default=1.0) ceiling_break_count = Column(Integer, default=0) ceiling_break_session = Column(Integer, default=0) consecutive_loss_value = Column(Float, default=0.0) consecutive_loss_count = Column(Integer, default=0) comeback_active = Column(Boolean, default=False) comeback_openings_left = Column(Integer, default=0) comeback_multiplier = Column(Float, default=1.0) hot_score = Column(Float, default=0.0) last_activity_date = Column(DateTime, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) user = relationship("User", back_populates="rpu_settings") class PromoCode(Base): __tablename__ = "promo_codes" __table_args__ = (Index("ix_promo_codes_code", "code"), Index("ix_promo_codes_active", "is_active")) id = Column(Integer, primary_key=True) code = Column(String(50), unique=True, nullable=False) reward_type = Column(String(30), nullable=False) reward_amount = Column(Float, default=0.0) reward_data = Column(String(500), default="") max_uses = Column(Integer, default=1) used_count = Column(Integer, default=0) start_date = Column(DateTime, default=datetime.utcnow) end_date = Column(DateTime, nullable=True) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.utcnow) created_by = Column(Integer, nullable=True) class TransactionLog(Base): __tablename__ = "transaction_log" __table_args__ = ( Index("ix_txlog_user_id", "user_id"), Index("ix_txlog_created", "created_at"), Index("ix_txlog_user_created", "user_id", "created_at"), ) id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) tx_type = Column(String(30), nullable=False) amount = Column(Float, default=0.0) fee = Column(Float, default=0.0) promo_code = Column(String(50), nullable=True) item_name = Column(String(200), nullable=True) inventory_item_id = Column(Integer, nullable=True) details = Column(String(500), default="") created_at = Column(DateTime, default=datetime.utcnow) user = relationship("User", back_populates="transactions") class InventoryItem(Base): __tablename__ = "inventory_items" __table_args__ = ( Index("ix_inventory_user_id", "user_id"), Index("ix_inventory_user_obtained", "user_id", "obtained_at"), ) id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) item_id = Column(Integer, nullable=False) market_hash_name = Column(String(200), nullable=False) rarity = Column(String(50)) wear = Column(String(50)) float_value = Column(Float, default=0.0) type = Column(String(20), default="Normal") obtained_from = Column(String(100)) obtained_at = Column(DateTime, default=datetime.utcnow) is_equipped = Column(Boolean, default=False) image_url = Column(String(500), default="") price_rub = Column(Float, default=0.0) user = relationship("User", back_populates="inventory_items") class CaseOpening(Base): __tablename__ = "case_openings" __table_args__ = ( Index("ix_case_openings_user_id", "user_id"), Index("ix_case_openings_opened_at", "opened_at"), Index("ix_case_openings_user_opened", "user_id", "opened_at"), ) id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) case_name = Column(String(100), nullable=False) item_id = Column(Integer, nullable=False) item_name = Column(String(200), nullable=False) rarity = Column(String(50)) float_value = Column(Float) opened_at = Column(DateTime, default=datetime.utcnow) user = relationship("User", back_populates="case_openings") class Contract(Base): __tablename__ = "contracts" __table_args__ = ( Index("ix_contracts_user_id", "user_id"), Index("ix_contracts_user_created", "user_id", "created_at"), ) id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) input_item_ids = Column(Text, nullable=False) output_item_id = Column(Integer, nullable=False) output_item_name = Column(String(200)) output_float = Column(Float) probability = Column(Float) created_at = Column(DateTime, default=datetime.utcnow) user = relationship("User", back_populates="contracts") class Achievement(Base): __tablename__ = "achievements" id = Column(Integer, primary_key=True) name = Column(String(100), unique=True, nullable=False) title = Column(String(200), nullable=False) description = Column(String(500)) icon = Column(String(50), default="🏆") category = Column(String(50), default="general") requirement_type = Column(String(50), nullable=False) requirement_value = Column(Integer, nullable=False) reward_amount = Column(Float, default=0.0) sort_order = Column(Integer, default=0) hidden = Column(Boolean, default=False) user_achievements = relationship("UserAchievement", back_populates="achievement") class UserAchievement(Base): __tablename__ = "user_achievements" __table_args__ = ( Index("ix_user_achievements_user", "user_id"), Index("ix_user_achievements_ach", "achievement_id"), Index("ix_user_achievements_both", "user_id", "achievement_id", unique=True), ) id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) achievement_id = Column(Integer, ForeignKey("achievements.id"), nullable=False) progress = Column(Integer, default=0) unlocked_at = Column(DateTime, nullable=True) created_at = Column(DateTime, default=datetime.utcnow) user = relationship("User", backref="user_achievements") achievement = relationship("Achievement", back_populates="user_achievements") class ActivityFeed(Base): __tablename__ = "activity_feed" __table_args__ = ( Index("ix_activity_user_id", "user_id"), Index("ix_activity_created", "created_at"), ) id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) username = Column(String(50), nullable=False) activity_type = Column(String(50), nullable=False) message = Column(String(500), nullable=False) data_json = Column(Text, default="{}") created_at = Column(DateTime, default=datetime.utcnow) user = relationship("User", backref="activities") # ─── Create tables ────────────────────────────────────────────────────────── def init_sync_db(): Base.metadata.create_all(bind=engine) _run_migrations(engine, is_sync=True) async def init_async_db(): async with async_engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) def _run_migrations(engine_or_conn, is_sync: bool): try: from sqlalchemy import inspect as sa_inspect inspector = sa_inspect(engine_or_conn) if is_sync else sa_inspect(engine_or_conn) existing_tables = inspector.get_table_names() for tbl_name in Base.metadata.tables: if tbl_name not in existing_tables: continue model_cols = {c.name for c in Base.metadata.tables[tbl_name].columns} db_cols = {c["name"] for c in inspector.get_columns(tbl_name)} missing = model_cols - db_cols if missing: print(f"[DB] Migrating {tbl_name}: adding {missing}") except Exception as e: print(f"[DB] Migration check: {e}") # ─── Session helpers ──────────────────────────────────────────────────────── def get_db() -> Generator: db = SessionLocal() try: yield db finally: db.close() async def get_async_db() -> AsyncGenerator[AsyncSession, None]: async with AsyncSessionLocal() as db: try: yield db finally: await db.close() # ─── Thread-safe sync session wrapper for async endpoints ────────────────── # Use this as a bridge: runs sync DB ops in executor to avoid blocking event loop. # The session is closed when the context exits. import asyncio from contextlib import asynccontextmanager @asynccontextmanager async def get_db_executor(): """Async context manager wrapping sync SessionLocal in executor thread. Use for endpoints that haven't been fully async-converted yet.""" loop = asyncio.get_running_loop() db = await loop.run_in_executor(None, lambda: SessionLocal()) try: yield db finally: await loop.run_in_executor(None, db.close)