mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-07-31 07:49:23 +00:00
- Added a type ignore comment for the assignment of optionxform in config_schema.py to suppress type checker warnings. - Updated path inference logic to return None when bot latitude or longitude is not set, ensuring safer handling of missing values. - Enhanced the decoding of path nodes to check for None before appending repeater details, preventing potential errors.
176 lines
7.0 KiB
Python
176 lines
7.0 KiB
Python
"""Settings persistence abstraction for plugin/command configuration.
|
|
|
|
``config.ini`` is the source of truth today, but we are building toward an
|
|
optional ``[Bot] persist_settings = config.ini|database`` switch. This module
|
|
defines a single :class:`SettingsStore` interface with two backends:
|
|
|
|
- :class:`ConfigIniStore` — reads/writes the INI file, using the
|
|
comment-preserving writer in :mod:`modules.ini_writer`. Wired and working.
|
|
- :class:`DatabaseStore` — a documented stub for a future ``plugin_settings``
|
|
table. Not implemented this iteration; :func:`get_settings_store` falls back
|
|
to the INI backend with a warning if ``persist_settings = database``.
|
|
|
|
No Flask or bot-core dependencies; pass a ``configparser`` object and a path in.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import configparser
|
|
import logging
|
|
from abc import ABC, abstractmethod
|
|
from typing import Optional
|
|
|
|
from modules.ini_writer import update_ini_values
|
|
|
|
logger = logging.getLogger("MeshCoreBot")
|
|
|
|
|
|
class SettingsStore(ABC):
|
|
"""Backend-agnostic read/write interface for plugin settings."""
|
|
|
|
@abstractmethod
|
|
def section_exists(self, section: str) -> bool:
|
|
...
|
|
|
|
@abstractmethod
|
|
def read_section(self, section: str) -> dict[str, str]:
|
|
"""Return all key/value pairs in a section (empty dict if absent)."""
|
|
|
|
@abstractmethod
|
|
def get_value(self, section: str, key: str, fallback: Optional[str] = None) -> Optional[str]:
|
|
...
|
|
|
|
@abstractmethod
|
|
def write_values(self, section: str, values: dict[str, str]) -> dict:
|
|
"""Persist ``values`` under ``section``.
|
|
|
|
Returns a summary dict (backend-specific). For the INI backend this is
|
|
the :func:`modules.ini_writer.update_ini_values` result, which includes
|
|
``backup_path``.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def write_sections(
|
|
self,
|
|
updates: dict[str, dict[str, str]],
|
|
deletes: Optional[dict[str, list[str] | set[str]]] = None,
|
|
) -> dict:
|
|
"""Persist multiple sections at once, with optional key deletions.
|
|
|
|
Used by dynamic-list editors that span more than one section (e.g. a
|
|
plugin's own section plus a managed list section) so the whole save is
|
|
one atomic write with a single backup.
|
|
"""
|
|
|
|
|
|
class ConfigIniStore(SettingsStore):
|
|
"""INI-backed store. Keeps the in-memory ``config`` object in sync so the
|
|
owning process reads fresh values immediately after a write."""
|
|
|
|
def __init__(self, config: configparser.ConfigParser, config_path: str) -> None:
|
|
self.config = config
|
|
self.config_path = config_path
|
|
|
|
def section_exists(self, section: str) -> bool:
|
|
return self.config.has_section(section)
|
|
|
|
def read_section(self, section: str) -> dict[str, str]:
|
|
if not self.config.has_section(section):
|
|
return {}
|
|
return dict(self.config.items(section))
|
|
|
|
def get_value(self, section: str, key: str, fallback: Optional[str] = None) -> Optional[str]:
|
|
return self.config.get(section, key, fallback=fallback)
|
|
|
|
def write_values(self, section: str, values: dict[str, str]) -> dict:
|
|
# Update the in-memory config first so this process is consistent.
|
|
# Guard each set: a value containing '%' trips BasicInterpolation's
|
|
# before_set validation; the on-disk write below is the source of truth
|
|
# and is interpolation-free, so we skip the in-memory copy for such keys.
|
|
if not self.config.has_section(section):
|
|
self.config.add_section(section)
|
|
for key, value in values.items():
|
|
try:
|
|
self.config.set(section, key, value)
|
|
except ValueError:
|
|
logger.debug("Skipped in-memory cache of %s.%s (interpolation char)", section, key)
|
|
# Persist to disk, preserving comments and taking a backup.
|
|
return update_ini_values(self.config_path, {section: values})
|
|
|
|
def write_sections(
|
|
self,
|
|
updates: dict[str, dict[str, str]],
|
|
deletes: Optional[dict[str, list[str] | set[str]]] = None,
|
|
) -> dict:
|
|
# Mirror writes into the in-memory config so this process stays current.
|
|
for section, values in (updates or {}).items():
|
|
if not self.config.has_section(section):
|
|
self.config.add_section(section)
|
|
for key, value in values.items():
|
|
try:
|
|
self.config.set(section, key, value)
|
|
except ValueError:
|
|
logger.debug("Skipped in-memory cache of %s.%s (interpolation char)", section, key)
|
|
for section, keys in (deletes or {}).items():
|
|
if self.config.has_section(section):
|
|
for key in keys:
|
|
self.config.remove_option(section, key)
|
|
return update_ini_values(self.config_path, updates, deletes)
|
|
|
|
|
|
class DatabaseStore(SettingsStore):
|
|
"""STUB — future DB-backed store for ``persist_settings = database``.
|
|
|
|
Designed against a ``plugin_settings`` table::
|
|
|
|
CREATE TABLE plugin_settings (
|
|
section TEXT NOT NULL,
|
|
key TEXT NOT NULL,
|
|
value TEXT,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (section, key)
|
|
);
|
|
|
|
(add ``'plugin_settings'`` to ``DBManager.ALLOWED_TABLES`` and a migration
|
|
when implementing). Until then, callers should not construct this directly;
|
|
:func:`get_settings_store` returns the INI backend instead.
|
|
"""
|
|
|
|
def __init__(self, db_manager) -> None: # pragma: no cover - stub
|
|
self.db_manager = db_manager
|
|
|
|
def section_exists(self, section: str) -> bool: # pragma: no cover - stub
|
|
raise NotImplementedError("DatabaseStore is not implemented yet")
|
|
|
|
def read_section(self, section: str) -> dict[str, str]: # pragma: no cover - stub
|
|
raise NotImplementedError("DatabaseStore is not implemented yet")
|
|
|
|
def get_value(self, section, key, fallback=None): # pragma: no cover - stub
|
|
raise NotImplementedError("DatabaseStore is not implemented yet")
|
|
|
|
def write_values(self, section: str, values: dict[str, str]) -> dict: # pragma: no cover - stub
|
|
raise NotImplementedError("DatabaseStore is not implemented yet")
|
|
|
|
def write_sections(self, updates, deletes=None) -> dict: # pragma: no cover - stub
|
|
raise NotImplementedError("DatabaseStore is not implemented yet")
|
|
|
|
|
|
def get_settings_store(
|
|
config: configparser.ConfigParser,
|
|
config_path: str,
|
|
db_manager=None,
|
|
) -> SettingsStore:
|
|
"""Return the configured settings store.
|
|
|
|
Reads ``[Bot] persist_settings`` (default ``config.ini``). The ``database``
|
|
backend is not implemented yet, so it logs a warning and falls back to the
|
|
INI store, ensuring writes are never silently dropped.
|
|
"""
|
|
mode = (config.get("Bot", "persist_settings", fallback="config.ini") or "").strip().lower()
|
|
if mode == "database":
|
|
logger.warning(
|
|
"persist_settings = database requested, but the database backend is "
|
|
"not implemented yet; falling back to config.ini"
|
|
)
|
|
return ConfigIniStore(config, config_path)
|