refactor(config): Settings i JsonFileStore jako podstawa pakietu
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
edf317d21e
commit
e194a8611c
4 files changed
+102
No files matched your search
@@ -0,0 +1,5 @@
|
||||
"""ŻyrarBOT - bot muzyczny Discord z panelem webowym."""
|
||||
|
||||
__all__ = ["__version__"]
|
||||
|
||||
__version__ = "2.0.0"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
"""Trwałe składowanie danych: pliki JSON oraz katalog uploadów."""
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user