260 lines
9.7 KiB
Python
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
|