Source code for granicus_archiver.web.config

from __future__ import annotations
from typing import Sequence, Literal, ClassVar, Self, Any
from pathlib import Path
from dataclasses import dataclass, field
from os import PathLike

from aiohttp import web
from yarl import URL
from yaml import (
    load as yaml_load,
    dump as yaml_dump,
    CLoader as YamlLoader,
    CDumper as YamlDumper,
)

from ..clips.model import Location
from .types import NavLink
from ..config import BaseConfig

__all__ = ('AppConfig', 'APP_CONF_KEY')


type ListFilterField = Literal[
    "clip_status", "category", "agenda_status", "minutes_status",
]
"""Type for list filter fields that can be hidden in the UI"""


[docs] @dataclass class AppConfig(BaseConfig): """Web app configuration """ hostname: str = 'localhost' """Hostname to bind to""" port: int = 8080 """Port to bind to""" sockfile: Path|None = None """UNIX socket file to bind to If ``None``, the :attr:`hostname` and :attr:`port` will be used instead. """ serve_static: bool = True """Whether to serve static files directly from aiohttp""" read_only: bool = True """If ``True``, the app will not allow modification of any data files""" static_url: URL = URL('/') """Root URL to serve static files from If :attr:`serve_static` is ``True``, this should be ``"/"``. Otherwise, it should be the URL path to the static files. """ use_s3: bool = False """If ``True``, the app will use S3 to for data files and assets""" s3_data_dir: Path|None = None """Root directory to store local data files from s3""" nav_links: Sequence[NavLink] = ( NavLink(name='home', title='Home', url='home'), NavLink(name='legistar', title='Legistar', url='rguid_legistar_items'), NavLink(name='clips', title='Clips', url='clip_list'), ) """Navigation links for the app""" index_nav_link_name: str = 'home' """Name of the nav link in :attr:`nav_links` to use for the index page (home page)""" site_name: str = 'Granicus Archive' """Name of the site""" hidden_clip_categories: Sequence[Location] = field(default_factory=list) """List of clip categories to hide in the UI""" hidden_clip_list_filters: Sequence[ListFilterField] = field(default_factory=list) """List of clip list filters to hide in the UI""" group_key: ClassVar[str] = 'web'
[docs] @classmethod def load(cls, filename: PathLike) -> Self: """Load the configuration from a file """ if not isinstance(filename, Path): filename = Path(filename) if not filename.exists(): return cls.build_defaults() data = yaml_load(filename.read_text(), Loader=YamlLoader) return cls.deserialize(data)
[docs] def save(self, filename: PathLike) -> None: """Save the configuration to a file """ if not isinstance(filename, Path): filename = Path(filename) filename.parent.mkdir(parents=True, exist_ok=True) filename.write_text(yaml_dump(self.serialize(), Dumper=YamlDumper))
[docs] def update(self, **kwargs) -> bool: updated = False for k, v in kwargs.items(): if getattr(self, k) == v: continue setattr(self, k, v) updated = True return updated
[docs] @classmethod def build_defaults(cls, **kwargs) -> Self: return cls(**kwargs)
def serialize(self) -> dict[str, Any]: return { 'hostname': self.hostname, 'port': self.port, 'sockfile': str(self.sockfile) if self.sockfile is not None else None, 'serve_static': self.serve_static, 'read_only': self.read_only, 'static_url': str(self.static_url), 'use_s3': self.use_s3, 's3_data_dir': self.s3_data_dir, 'nav_links': [nl.serialize() for nl in self.nav_links], 'index_nav_link_name': self.index_nav_link_name, 'hidden_clip_categories': self.hidden_clip_categories, 'hidden_clip_list_filters': self.hidden_clip_list_filters, 'site_name': self.site_name, } @classmethod def deserialize(cls, data: dict[str, Any]) -> Self: return cls( hostname=data['hostname'], port=data['port'], sockfile=Path(data['sockfile']) if data['sockfile'] is not None else None, serve_static=data['serve_static'], read_only=data['read_only'], static_url=URL(data['static_url']), use_s3=data['use_s3'], s3_data_dir=data['s3_data_dir'], nav_links=[NavLink.deserialize(nl) for nl in data['nav_links']], index_nav_link_name=data.get('index_nav_link_name', 'home'), hidden_clip_categories=[ Location(c) for c in data.get('hidden_clip_categories', []) ], hidden_clip_list_filters=data.get('hidden_clip_list_filters', []), site_name=data['site_name'], )
[docs] @classmethod def load_from_env(cls) -> Self: hidden_clip_list_filters = cls._get_env_var_list('hidden_clip_list_filters', str) if hidden_clip_list_filters is None: hidden_clip_list_filters = [] index_nav_link_name = cls._get_env_var('index_nav_link_name', str) if not index_nav_link_name: index_nav_link_name = 'home' kw = dict( hostname=cls._get_env_var('hostname', str), port=cls._get_env_var('port', int), sockfile=cls._get_env_var('sockfile', Path), serve_static=cls._get_env_var('serve_static', bool), read_only=cls._get_env_var('read_only', bool), static_url=cls._get_env_var('static_url', URL), use_s3=cls._get_env_var('use_s3', bool), s3_data_dir=cls._get_env_var('s3_data_dir', Path), index_nav_link_name=index_nav_link_name, hidden_clip_categories=cls._get_env_var_list('hidden_clip_categories', str), hidden_clip_list_filters=hidden_clip_list_filters, site_name=cls._get_env_var('site_name', str), ) kw = {k: v for k, v in kw.items() if v is not None} return cls.build_defaults(**kw)
APP_CONF_KEY = web.AppKey('AppConfig', AppConfig) """App key for the :class:`AppConfig` instance"""