misc-csechet/frontools/config.py

180 lines
5.5 KiB
Python

"""Config loading"""
from gettext import gettext as _
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
from re import compile as re_compile
from typing import Iterable, Optional
from xdg import xdg_config_dirs
from frontools.cache import Cache, DataCache, NullCache, ObjectCache
from frontools.sources import CachedSource, OverrideSource, Source
from frontools.utils import ErrorSummary
REMOTE_SOURCE_NAME = "remote"
class ConfigError(Exception):
"""Error raised on config error"""
class SiteConfig:
"""Configuration object for a particular website"""
urls: list[str]
def __init__(self, urls: Iterable[str]):
self.urls = list(urls)
class Config:
"""Configuration object"""
def __init__(
self,
use_cache: bool,
default_source_name: Optional[str],
include_urls: list[str],
exclude_urls: list[str],
):
self._use_cache = use_cache
self._sources: dict[str, Source] = {}
self._sites: dict[str, SiteConfig] = {}
if default_source_name is None:
self._default_source_name = REMOTE_SOURCE_NAME
else:
self._default_source_name = default_source_name
self._error_summary = ErrorSummary()
remote_cache = self.get_data_cache(REMOTE_SOURCE_NAME)
self._add_source(
REMOTE_SOURCE_NAME, CachedSource(self._error_summary, remote_cache)
)
self._include_urls = [re_compile(it) for it in include_urls]
self._exclude_urls = [re_compile(it) for it in exclude_urls]
@staticmethod
async def load(
config_path: Optional[Path],
default_source_name: Optional[str],
use_cache: bool,
include_urls: list[str],
exclude_urls: list[str],
) -> "Config":
"""Load config from the given path"""
config = Config(use_cache, default_source_name, include_urls, exclude_urls)
if config_path is None:
config_path = _find_config()
if config_path is not None:
module_spec = spec_from_file_location("frontools.local_config", config_path)
assert module_spec is not None
config_module = module_from_spec(module_spec)
assert module_spec is not None
module_spec.loader.exec_module(config_module) # type: ignore
if not hasattr(config_module, "CONFIG"):
raise ConfigError(
_(
f"Configuration file {config_path} should define a root variable CONFIG."
)
)
config_loader = getattr(config_module, "CONFIG")
await config_loader(config)
return config
@property
def remote_source(self) -> Source:
"""get the default source for this context"""
return self.get_source(REMOTE_SOURCE_NAME)
@property
def default_source(self) -> Source:
"""get the default source for this context"""
return self.get_source(self._default_source_name)
@property
def sites(self) -> Iterable[tuple[str, SiteConfig]]:
"""Return sites configured for this context"""
return self._sites.items()
@property
def config_cache(self) -> Cache[object]:
"""Get the cache for configuration"""
return self.get_object_cache("config")
def add_site_url(self, name: str, url: str) -> None:
"""Add an url for a site"""
if len(self._include_urls):
if all([not it.match(url) for it in self._include_urls]):
return
if len(self._exclude_urls):
if any([it.match(url) for it in self._exclude_urls]):
return
if name not in self._sites:
self._sites[name] = SiteConfig([])
self._sites[name].urls.append(url)
def get_data_cache(self, name: str) -> Cache[bytes]:
"""Get a data cache with the given identifier"""
if self._use_cache:
return DataCache(name)
return NullCache()
def get_object_cache(self, name: str) -> Cache[object]:
"""Get an object cache with the given identifier"""
if self._use_cache:
return ObjectCache[object](name)
return NullCache()
def add_override_source(
self,
name: str,
mappings: list[tuple[str, str]],
next_source_name: Optional[str] = None,
) -> None:
"""Add a source overriding given patterns"""
assert name not in self._sources
if next_source_name is None:
next_source = self.default_source
else:
next_source = self.get_source(next_source_name)
self._sources[name] = OverrideSource(self._error_summary, mappings, next_source)
def get_source(self, name: str) -> Source:
"""Get an alternate source in the configured ones"""
if name not in self._sources:
raise Exception(f"No source configured matching {name}")
return self._sources[name]
def echo_error_summary(self) -> None:
self._error_summary.echo()
def _add_source(self, name: str, source: Source) -> None:
if name in self._sources:
raise Exception(f"Source {name} already configured")
self._sources[name] = source
def _find_config() -> Optional[Path]:
local_config = Path(".frontools.py")
if local_config.is_file():
return local_config
for config_dir in xdg_config_dirs():
config_path = config_dir / "frontools/config.py"
if config_path.is_file():
return config_path
return None