itch-dl/itch_dl/config.py
Ryszard Knop cb08443778 Upgrade to Python 3.10+
We can use fancier type annotations now, newer deps, more checkers.
2025-02-14 15:41:15 +01:00

107 lines
3.3 KiB
Python

import os
import json
import logging
import platform
import argparse
from dataclasses import dataclass, fields
from typing import Any, get_type_hints
import requests
from . import __version__
@dataclass
class Settings:
"""Available settings for itch-dl. Make sure all of them
have default values, as the config file may not exist."""
api_key: str | None = None
user_agent: str = f"python-requests/{requests.__version__} itch-dl/{__version__}"
download_to: str | None = None
mirror_web: bool = False
urls_only: bool = False
parallel: int = 1
filter_files_glob: str | None = None
filter_files_regex: str | None = None
verbose: bool = False
def create_and_get_config_path() -> str:
"""Returns the configuration directory in the appropriate
location for the current OS. The directory may not exist."""
system = platform.system()
if system == "Linux":
base_path = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config/")
elif system == "Darwin":
base_path = os.path.expanduser("~/Library/Application Support/")
elif system == "Windows":
base_path = os.environ.get("APPDATA") or os.path.expanduser("~/AppData/Roaming/")
else:
raise NotImplementedError(f"Unknown platform: {system}")
return os.path.join(base_path, "itch-dl")
def clean_config(config_data: dict[str, Any]) -> dict[str, Any]:
cleaned_config = {}
settings_invalid = False
type_hints = get_type_hints(Settings)
# Complain about invalid types, if any:
for key, value in config_data.items():
if not (expected_type := type_hints.get(key)):
logging.warning("Settings contain an unknown item, ignoring: '%s'", key)
continue
if not isinstance(value, expected_type):
logging.fatal("Settings.%s has invalid type '%s', expected '%s'", key, type(value), expected_type)
# Keep iterating to look up all the bad keys:
settings_invalid = True
continue
cleaned_config[key] = value
if settings_invalid:
logging.fatal("Settings invalid, bailing out!")
exit(1)
return cleaned_config
def load_config(args: argparse.Namespace, profile: str | None = None) -> Settings:
"""Loads the configuration from the file system if it exists,
the returns a Settings object."""
config_path = create_and_get_config_path()
config_file_path = os.path.join(config_path, "config.json")
profile_file_path = os.path.join(config_path, "profiles", profile or "")
if os.path.isfile(config_file_path):
logging.debug("Found config file: %s", config_file_path)
with open(config_file_path) as f:
config_data = json.load(f)
else:
config_data = {}
if os.path.isfile(profile_file_path):
logging.debug("Found profile: %s", profile_file_path)
with open(config_file_path) as f:
profile_data = json.load(f)
config_data.update(profile_data)
# All settings from the base file:
settings = Settings(**clean_config(config_data))
# Apply overrides from CLI args on each field in Settings:
for field in fields(Settings):
key = field.name
if value := getattr(args, key):
setattr(settings, key, value)
return settings