from __future__ import annotations from typing import Any from db import get_connection DEFAULT_APP_SETTINGS = { "default_parser_source_key": "SUPERLEAGUE", "rfs_base_url": "https://wfl.rfs.ru", } DEFAULT_PARSER_SOURCES = [ { "key": "SUPERLEAGUE", "title": "Суперлига 2026", "tournament_id": "1061879", "round_id": "1117550", "season": "2025/2026", "calendar_type": "tours", "logo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Teams Logos", "photo_base_path": r"D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo", "teams_url": "https://wfl.rfs.ru/tournament/1061879/teams", "schedule_url": "https://wfl.rfs.ru/tournament/1061879/calendar?round_id=1117550&type=tours", "standings_url": "https://wfl.rfs.ru/tournament/1061879/tables", "match_base_url": "https://wfl.rfs.ru/match/", "base_url": "https://wfl.rfs.ru", "sort_order": 10, "is_active": True, }, { "key": "RUSSIAN_CUP", "title": "Кубок России 2026", "tournament_id": "1064908", "round_id": "1125159", "season": "2026", "calendar_type": "stages", "logo_base_path": r"D:\Графика\ФУТБОЛ\Кубок России 2026\Teams Logos", "photo_base_path": r"D:\Графика\ФУТБОЛ\Кубок России 2026\Photo", "teams_url": "https://wfl.rfs.ru/tournament/1064908/teams", "schedule_url": "https://wfl.rfs.ru/tournament/1064908/calendar?round_id=1125159&type=stages", "standings_url": "https://wfl.rfs.ru/tournament/1064908/tables", "match_base_url": "https://wfl.rfs.ru/match/", "base_url": "https://wfl.rfs.ru", "sort_order": 20, "is_active": True, }, ] def _row_to_source(row: tuple) -> dict[str, Any]: return { "key": row[0] or "", "title": row[1] or "", "tournament_id": row[2] or "", "round_id": row[3] or "", "season": row[4] or "", "calendar_type": row[5] or "tours", "logo_base_path": row[6] or "", "photo_base_path": row[7] or "", "teams_url": row[8] or "", "schedule_url": row[9] or "", "standings_url": row[10] or "", "match_base_url": row[11] or "", "base_url": row[12] or "https://wfl.rfs.ru", "sort_order": row[13] or 0, "is_active": bool(row[14]), } def ensure_project_settings_tables() -> None: """Создаёт таблицы настроек проекта и добавляет стандартные источники, если их ещё нет.""" conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ CREATE TABLE IF NOT EXISTS app_settings ( key VARCHAR(100) PRIMARY KEY, value TEXT NOT NULL DEFAULT '', updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); """ ) cur.execute( """ CREATE TABLE IF NOT EXISTS parser_sources ( key VARCHAR(50) PRIMARY KEY, title VARCHAR(255) NOT NULL, tournament_id VARCHAR(100), round_id VARCHAR(100), season VARCHAR(50), calendar_type VARCHAR(50) NOT NULL DEFAULT 'tours', logo_base_path TEXT, photo_base_path TEXT, teams_url TEXT, schedule_url TEXT, standings_url TEXT, match_base_url TEXT, base_url TEXT NOT NULL DEFAULT 'https://wfl.rfs.ru', sort_order INTEGER NOT NULL DEFAULT 100, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP NOT NULL DEFAULT NOW(), updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); """ ) cur.execute("ALTER TABLE parser_sources ADD COLUMN IF NOT EXISTS photo_base_path TEXT;") for key, value in DEFAULT_APP_SETTINGS.items(): cur.execute( """ INSERT INTO app_settings (key, value, updated_at) VALUES (%s, %s, NOW()) ON CONFLICT (key) DO NOTHING; """, (key, value), ) for source in DEFAULT_PARSER_SOURCES: cur.execute( """ INSERT INTO parser_sources ( key, title, tournament_id, round_id, season, calendar_type, logo_base_path, photo_base_path, teams_url, schedule_url, standings_url, match_base_url, base_url, sort_order, is_active, created_at, updated_at ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) ON CONFLICT (key) DO NOTHING; """, ( source["key"], source["title"], source["tournament_id"], source["round_id"], source["season"], source["calendar_type"], source["logo_base_path"], source["photo_base_path"], source["teams_url"], source["schedule_url"], source["standings_url"], source["match_base_url"], source["base_url"], source["sort_order"], source["is_active"], ), ) for source in DEFAULT_PARSER_SOURCES: cur.execute( """ UPDATE parser_sources SET photo_base_path = %s, updated_at = NOW() WHERE key = %s AND (photo_base_path IS NULL OR TRIM(photo_base_path) = ''); """, (source.get("photo_base_path") or "", source["key"]), ) conn.commit() except Exception: conn.rollback() raise finally: conn.close() def get_app_settings() -> dict[str, str]: conn = get_connection() try: with conn.cursor() as cur: cur.execute("SELECT key, value FROM app_settings;") rows = cur.fetchall() settings = dict(DEFAULT_APP_SETTINGS) settings.update({row[0]: row[1] or "" for row in rows}) return settings finally: conn.close() def get_app_setting(key: str, default: str = "") -> str: return get_app_settings().get(key, default) def update_app_setting(key: str, value: str) -> None: conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ INSERT INTO app_settings (key, value, updated_at) VALUES (%s, %s, NOW()) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW(); """, (key, value or ""), ) conn.commit() except Exception: conn.rollback() raise finally: conn.close() def list_parser_sources_from_db(active_only: bool = True) -> list[dict[str, Any]]: conn = get_connection() try: with conn.cursor() as cur: if active_only: cur.execute( """ SELECT key, title, tournament_id, round_id, season, calendar_type, logo_base_path, photo_base_path, teams_url, schedule_url, standings_url, match_base_url, base_url, sort_order, is_active FROM parser_sources WHERE is_active = TRUE ORDER BY sort_order ASC, title ASC; """ ) else: cur.execute( """ SELECT key, title, tournament_id, round_id, season, calendar_type, logo_base_path, photo_base_path, teams_url, schedule_url, standings_url, match_base_url, base_url, sort_order, is_active FROM parser_sources ORDER BY sort_order ASC, title ASC; """ ) rows = cur.fetchall() return [_row_to_source(row) for row in rows] finally: conn.close() def get_parser_source_from_db(source_key: str) -> dict[str, Any] | None: source_key = (source_key or "").upper().strip() if not source_key: return None conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ SELECT key, title, tournament_id, round_id, season, calendar_type, logo_base_path, photo_base_path, teams_url, schedule_url, standings_url, match_base_url, base_url, sort_order, is_active FROM parser_sources WHERE key = %s LIMIT 1; """, (source_key,), ) row = cur.fetchone() return _row_to_source(row) if row else None finally: conn.close() def update_parser_source(source_key: str, values: dict[str, str]) -> None: source_key = (source_key or "").upper().strip() if not source_key: raise ValueError("Не указан ключ источника") current = get_parser_source_from_db(source_key) if not current: current = next((item for item in DEFAULT_PARSER_SOURCES if item["key"] == source_key), None) if not current: current = { "key": source_key, "title": source_key, "tournament_id": "", "round_id": "", "season": "", "calendar_type": "tours", "logo_base_path": "", "photo_base_path": "", "teams_url": "", "schedule_url": "", "standings_url": "", "match_base_url": "", "base_url": DEFAULT_APP_SETTINGS["rfs_base_url"], "sort_order": 100, "is_active": True, } merged = {**current, **{key: value for key, value in values.items() if value is not None}} merged["key"] = source_key merged["calendar_type"] = merged.get("calendar_type") or "tours" merged["base_url"] = merged.get("base_url") or DEFAULT_APP_SETTINGS["rfs_base_url"] conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ INSERT INTO parser_sources ( key, title, tournament_id, round_id, season, calendar_type, logo_base_path, photo_base_path, teams_url, schedule_url, standings_url, match_base_url, base_url, sort_order, is_active, created_at, updated_at ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()) ON CONFLICT (key) DO UPDATE SET title = EXCLUDED.title, tournament_id = EXCLUDED.tournament_id, round_id = EXCLUDED.round_id, season = EXCLUDED.season, calendar_type = EXCLUDED.calendar_type, logo_base_path = EXCLUDED.logo_base_path, photo_base_path = EXCLUDED.photo_base_path, teams_url = EXCLUDED.teams_url, schedule_url = EXCLUDED.schedule_url, standings_url = EXCLUDED.standings_url, match_base_url = EXCLUDED.match_base_url, base_url = EXCLUDED.base_url, sort_order = EXCLUDED.sort_order, is_active = EXCLUDED.is_active, updated_at = NOW(); """, ( merged.get("key", source_key), merged.get("title") or source_key, merged.get("tournament_id") or "", merged.get("round_id") or "", merged.get("season") or "", merged.get("calendar_type") or "tours", merged.get("logo_base_path") or "", merged.get("photo_base_path") or "", merged.get("teams_url") or "", merged.get("schedule_url") or "", merged.get("standings_url") or "", merged.get("match_base_url") or "", merged.get("base_url") or DEFAULT_APP_SETTINGS["rfs_base_url"], int(merged.get("sort_order") or 100), bool(merged.get("is_active", True)), ), ) conn.commit() except Exception: conn.rollback() raise finally: conn.close()