Files
auto-huami-token/auto_huami_token/config_handler.py
T
2026-03-05 12:01:08 +01:00

260 lines
9.7 KiB
Python

#!/usr/bin/env python3
import json
import logging
import os
import sys
import typing
from argparse import Namespace
from collections import namedtuple
from dataclasses import dataclass
from getpass import getpass
from logging import StreamHandler
from types import SimpleNamespace
import psutil
from xdg import BaseDirectory as Xdg
class ConfigHandler:
def __init__(self, args: Namespace, log_name: str):
self.config = Config()
self.config_path = ConfigHandler.get_config_path(args)
new_config = self.load_config()
self.log = logging.getLogger(log_name)
config_change = (
self.configure_credentials(args)
| self.configure_target_dir(args)
| self.configure_logging(args)
)
if config_change and (
new_config or args.setup or (getattr(args, "save", False))
):
self.log.debug(
"saving changed config "
+ ("because of new config" if new_config else "from args")
)
self.save_config()
@staticmethod
def get_config_path(args: Namespace) -> str:
config_file_path: str = ""
if getattr(args, "config_path", False):
config_file_path = os.path.abspath(os.path.expanduser(args.config_path))
elif Xdg.load_first_config("auto-huami-token") and os.path.exists(
Xdg.load_first_config("auto-huami-token") + "/config.json"
):
config_file_path = (
Xdg.load_first_config("auto-huami-token") + "/config.json"
)
elif os.path.exists("./config.json"):
config_file_path = "./config.json"
elif (
os.getuid() == 0
or psutil.Process(os.getpid()).ppid() == 1
and os.path.exists("/etc/auto-huami-token/config.json")
):
config_file_path = "/etc/auto-huami-token/config.json"
if args.setup:
new_config_file_path = ConfigHandler.request_text_input_with_fallback(
"Where should the config file be saved?", config_file_path
)
if new_config_file_path:
config_file_path = new_config_file_path
return (
config_file_path
or Xdg.save_config_path("auto-huami-token") + "/config.json"
)
def configure_credentials(self, args: Namespace) -> bool:
config_change = args.setup
self.config.credentials = credentials = getattr(
self.config, "credentials", SimpleNamespace()
)
if not hasattr(credentials, "username") or args.setup:
current_username = getattr(credentials, "username", "")
credentials.username = ConfigHandler.request_text_input_with_fallback(
"Please enter your username.", current_username
)
self.log.debug("config.credentials.email has changed")
config_change = True
if not hasattr(self.config.credentials, "password") or args.setup:
credentials.password = ConfigHandler.request_password()
self.log.debug("config.credentials.password has changed")
config_change = True
return config_change
def configure_target_dir(self, args: Namespace) -> bool:
config_change = args.setup
if getattr(args, "target_dir", False):
abs_target_dir = os.path.abspath(os.path.expanduser(args.target_dir))
config_change = not hasattr(self.config, "target_dir") or (
self.config.target_dir != abs_target_dir
)
self.config.target_dir = abs_target_dir
self.log.debug("config.target_dir has changed")
elif not getattr(self.config, "target_dir", False):
self.config.target_dir = "."
if args.setup:
self.config.target_dir = ConfigHandler.request_text_input_with_fallback(
"Where should the downloaded file(s) be saved?", self.config.target_dir
)
self.config.target_dir = (
self.config.target_dir
if os.path.isabs(self.config.target_dir)
else os.path.abspath(os.path.expanduser(self.config.target_dir))
)
return config_change
def configure_logging(self, args: Namespace) -> bool:
config_change = False
if hasattr(args, "log_to") and args.log_to is not None:
self.config.log_to = args.log_to
if hasattr(args, "verbose") and args.verbose is not None:
self.config.verbose = args.verbose
if args.setup:
self.config.log_to = ConfigHandler.request_text_input_with_fallback(
"Where should logs be written, when not running as a systemd service? A path or 'disabled', which disables logging or '' to use the default location '~/.local/state/hf_cli'",
getattr(self.config, "log_to", ""),
)
self.config.verbose = ConfigHandler.request_boolean_with_fallback(
"Would you like to always log verbosely?",
getattr(self.config, "verbose", False),
)
if (
hasattr(self.config, "log_to")
and self.config.log_to is not None
and self.config.log_to != "disabled"
):
if "JOURNAL_STREAM" in os.environ:
from systemd.journal import JournalHandler
self.log.addHandler(JournalHandler())
else:
if (
self.config.log_to
and not os.path.isabs(self.config.log_to)
and (
os.path.exists(self.config.log_to)
or os.path.exists(os.path.dirname(self.config.log_to))
)
):
self.config.log_to = os.path.abspath(
os.path.expanduser(self.config.log_to)
)
self.config.log_to = (
self.config.log_to + "/auto-huami-token.log"
if os.path.isdir(self.config.log_to)
else self.config.log_to
)
if os.path.isabs(self.config.log_to):
from logging import FileHandler
self.config.log_to = self.config.log_to + (
"/auto-huami-token.log"
if os.path.isdir(self.config.log_to)
else ""
)
if not os.path.exists(os.path.dirname(self.config.log_to)):
os.makedirs(os.path.dirname(self.config.log_to), exist_ok=True)
self.log.addHandler(FileHandler(self.config.log_to))
config_change = True
elif self.config.log_to != "console":
from logging import FileHandler
self.config.log_to = (
Xdg.save_state_path("auto-huami-token")
+ "/auto-huami-token.log"
)
self.log.addHandler(FileHandler(self.config.log_to))
config_change = True
self.log.addHandler(StreamHandler(sys.stdout))
self.log.setLevel(
logging.DEBUG
if getattr(self.config, "verbose", False)
else logging.INFO
)
else:
self.log.propagate = False
self.log.disabled = True
config_change = (
args.setup
or hasattr(args, "log_to")
and args.log_to is not None
and self.config.log_to == "disabled"
)
return config_change
@property
def target_dir(self) -> str:
return self.config.target_dir
def load_config(self) -> bool:
self.config = self.load_config_json(self.config_path)
if self.config is None:
self.config = SimpleNamespace()
return True
return False
@staticmethod
def load_config_json(file_path: str):
try:
with open(file_path) as config_file:
return json.load(
config_file,
object_hook=lambda d: namedtuple("X", d.keys())(*d.values()),
)
except IOError:
return None
def save_config(self):
self.log.debug("Saving config")
if typing.TYPE_CHECKING:
from _typeshed import SupportsWrite
config_file: SupportsWrite[str]
with open(self.config_path, "w") as config_file:
json.dump(self.config, config_file, default=lambda x: vars(x))
@staticmethod
def request_password() -> str:
return getpass("Please enter your password: ")
@staticmethod
def request_boolean_with_fallback(question: str, current: bool = False) -> bool:
result = ConfigHandler.request_text_input_with_fallback(
f"{question} (yes/no) (currently {'yes' if current else 'no'}): ",
"yes" if current else "no",
).lower()
if not result.startswith(("y", "n")):
raise ValueError(
f"'{result}' is invalid. Only yes and no are valid choices"
)
return True if result.startswith("y") else False
@staticmethod
def request_text_input_with_fallback(question: str, current: str = "") -> str:
response = ConfigHandler.request_input(question + f" (currently '{current}'): ")
if not response:
return "" if current is None else current
return response
@staticmethod
def request_input(message: str):
return input(message)
@dataclass
class Credentials:
email: str
password: str
@dataclass
class Config:
target_dir: str
log_to: str
verbose: bool
credentials: Credentials