feat: initialize CCMA member administration

This commit is contained in:
Marcel Peterkau
2026-06-21 16:46:15 +02:00
parent 4c6a1191ee
commit dfd5b1192b
184 changed files with 5051 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
from __future__ import annotations
import json
import os
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
from ccma.storage.atomic import write_json_atomic
if TYPE_CHECKING:
from ccma.services.housekeeper import HousekeeperSettings
@dataclass(slots=True)
class AppConfig:
store_path: str = ""
gnucash_path: str = ""
theme_mode: str = "dark"
run_housekeeper_on_startup: bool = True
birthday_days_before: int = 7
birthday_days_after: int = 2
anniversary_days_before: int = 14
anniversary_days_after: int = 7
anniversary_intervals: str = "1Y;5Y;10Y;25Y;50Y"
window_geometry: str = ""
window_state: str = "normal"
monitor_bounds: tuple[int, int, int, int] | None = None
@property
def path(self) -> Path:
return config_directory() / "config.json"
def save(self) -> None:
write_json_atomic(
self.path,
{
"schema_version": 1,
"store_path": self.store_path,
"gnucash_path": self.gnucash_path,
"theme_mode": self.theme_mode,
"run_housekeeper_on_startup": self.run_housekeeper_on_startup,
"birthday_days_before": self.birthday_days_before,
"birthday_days_after": self.birthday_days_after,
"anniversary_days_before": self.anniversary_days_before,
"anniversary_days_after": self.anniversary_days_after,
"anniversary_intervals": self.anniversary_intervals,
"window_geometry": self.window_geometry,
"window_state": self.window_state,
"monitor_bounds": list(self.monitor_bounds) if self.monitor_bounds else None,
},
)
def housekeeper_settings(self) -> HousekeeperSettings:
from ccma.services.housekeeper import HousekeeperSettings
from ccma.services.intervals import IntervalValidationError
try:
return HousekeeperSettings.from_values(
birthday_days_before=self.birthday_days_before,
birthday_days_after=self.birthday_days_after,
anniversary_days_before=self.anniversary_days_before,
anniversary_days_after=self.anniversary_days_after,
anniversary_intervals=self.anniversary_intervals,
)
except IntervalValidationError:
return HousekeeperSettings()
def config_directory() -> Path:
override = os.environ.get("CCMA_CONFIG_DIR") or os.environ.get("C3MA_CONFIG_DIR")
if override:
return Path(override).expanduser()
if os.name == "nt":
return Path(os.environ.get("APPDATA", Path.home())) / "CCMA"
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "ccma"
def load_config() -> AppConfig:
path = config_directory() / "config.json"
if not path.exists() and not (os.environ.get("CCMA_CONFIG_DIR") or os.environ.get("C3MA_CONFIG_DIR")):
legacy_path = _legacy_config_directory() / "config.json"
if legacy_path.exists():
path = legacy_path
store_override = os.environ.get("CCMA_STORE") or os.environ.get("C3MA_STORE", "")
if not path.exists():
return AppConfig(store_path=store_override)
try:
data = json.loads(path.read_text(encoding="utf-8"))
monitor_raw = data.get("monitor_bounds")
monitor_bounds = None
if isinstance(monitor_raw, list) and len(monitor_raw) == 4:
monitor_bounds = tuple(int(value) for value in monitor_raw)
return AppConfig(
store_path=store_override or str(data.get("store_path", "")),
gnucash_path=str(data.get("gnucash_path", "")),
theme_mode=str(data.get("theme_mode", "dark")),
run_housekeeper_on_startup=bool(data.get("run_housekeeper_on_startup", True)),
birthday_days_before=int(data.get("birthday_days_before", 7)),
birthday_days_after=int(data.get("birthday_days_after", 2)),
anniversary_days_before=int(data.get("anniversary_days_before", 14)),
anniversary_days_after=int(data.get("anniversary_days_after", 7)),
anniversary_intervals=str(data.get("anniversary_intervals", "1Y;5Y;10Y;25Y;50Y")),
window_geometry=str(data.get("window_geometry", "")),
window_state=str(data.get("window_state", "normal")),
monitor_bounds=monitor_bounds,
)
except (OSError, ValueError, TypeError):
return AppConfig(store_path=store_override)
def _legacy_config_directory() -> Path:
if os.name == "nt":
return Path(os.environ.get("APPDATA", Path.home())) / "C3MA"
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "c3ma"