From e194a8611c59e470a961eaae5c6cfa0d1d2a1bd6 Mon Sep 17 00:00:00 2001 From: lgoose Date: Fri, 21 Aug 2026 01:09:52 +0200 Subject: [PATCH] refactor(config): Settings i JsonFileStore jako podstawa pakietu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Początek pakietu zyrarbot, budowanego wedle zasady jedna klasa = jeden plik. - Settings zbiera konfigurację ze zmiennych środowiskowych oraz ścieżki i limity, które dotąd były literałami rozsianymi po bot.py i web.py ("config/config.json", "uploads", zakres 1-999, domyślne 5 odtworzeń). - JsonFileStore zastępuje sześć niemal identycznych bloków open/json.load z ręcznym try/except. Brakujący lub uszkodzony plik daje pusty słownik zamiast wyjątku, a zapis sam tworzy katalog. Co-Authored-By: Claude Opus 5 --- zyrarbot/__init__.py | 5 +++ zyrarbot/settings.py | 43 +++++++++++++++++++++++ zyrarbot/storage/__init__.py | 1 + zyrarbot/storage/json_file_store.py | 53 +++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 zyrarbot/__init__.py create mode 100644 zyrarbot/settings.py create mode 100644 zyrarbot/storage/__init__.py create mode 100644 zyrarbot/storage/json_file_store.py diff --git a/zyrarbot/__init__.py b/zyrarbot/__init__.py new file mode 100644 index 0000000..bc3575f --- /dev/null +++ b/zyrarbot/__init__.py @@ -0,0 +1,5 @@ +"""ŻyrarBOT - bot muzyczny Discord z panelem webowym.""" + +__all__ = ["__version__"] + +__version__ = "2.0.0" diff --git a/zyrarbot/settings.py b/zyrarbot/settings.py new file mode 100644 index 0000000..7635818 --- /dev/null +++ b/zyrarbot/settings.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass + +from dotenv import load_dotenv + + +@dataclass(frozen=True) +class Settings: + """Konfiguracja aplikacji odczytana ze zmiennych środowiskowych.""" + + discord_token: str | None = None + guild_id: int | None = None + config_dir: str = "config" + uploads_dir: str = "uploads" + static_dir: str = "static" + web_host: str = "0.0.0.0" + web_port: int = 8000 + default_plays: int = 5 + history_limit: int = 50 + min_plays: int = 1 + max_plays: int = 999 + + @classmethod + def from_env(cls) -> Settings: + load_dotenv() + raw_guild_id = os.getenv("GUILD_ID") + return cls( + discord_token=os.getenv("DISCORD_TOKEN"), + guild_id=int(raw_guild_id) if raw_guild_id else None, + ) + + @property + def config_file(self) -> str: + return os.path.join(self.config_dir, "config.json") + + @property + def history_file(self) -> str: + return os.path.join(self.config_dir, "history.json") + + def is_valid_plays(self, value: int) -> bool: + return self.min_plays <= value <= self.max_plays diff --git a/zyrarbot/storage/__init__.py b/zyrarbot/storage/__init__.py new file mode 100644 index 0000000..1c4a0d9 --- /dev/null +++ b/zyrarbot/storage/__init__.py @@ -0,0 +1 @@ +"""Trwałe składowanie danych: pliki JSON oraz katalog uploadów.""" diff --git a/zyrarbot/storage/json_file_store.py b/zyrarbot/storage/json_file_store.py new file mode 100644 index 0000000..095dde2 --- /dev/null +++ b/zyrarbot/storage/json_file_store.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import json +import logging +import os +from typing import Any + +logger = logging.getLogger(__name__) + + +class JsonFileStore: + """Prosty magazyn słownika w pliku JSON, odporny na brakujący/uszkodzony plik.""" + + def __init__(self, path: str): + self._path = path + + @property + def path(self) -> str: + return self._path + + def read(self) -> dict[str, Any]: + if not os.path.exists(self._path): + return {} + try: + with open(self._path, "r", encoding="utf-8") as handle: + data = json.load(handle) + except (OSError, ValueError) as error: + logger.error("Nie udało się odczytać %s: %s", self._path, error) + return {} + return data if isinstance(data, dict) else {} + + def write(self, data: dict[str, Any]) -> bool: + directory = os.path.dirname(self._path) + try: + if directory: + os.makedirs(directory, exist_ok=True) + with open(self._path, "w", encoding="utf-8") as handle: + json.dump(data, handle, indent=4, ensure_ascii=False) + except OSError as error: + logger.error("Nie udało się zapisać %s: %s", self._path, error) + return False + return True + + def set(self, key: str, value: Any) -> bool: + data = self.read() + if value is None: + data.pop(key, None) + else: + data[key] = value + return self.write(data) + + def get(self, key: str, default: Any = None) -> Any: + return self.read().get(key, default)