def test_env(monkeypatch): from modconfig import Config # Config accepts modules path from ENV variables monkeypatch.setenv("MODCONFIG", "example.production") cfg = Config("ENV:MODCONFIG") assert cfg.DATABASE["host"] == "db.com" # Any var from config could be redefined in ENV monkeypatch.setenv("API_KEY", "prod_key") monkeypatch.setenv("DATABASE", "[1,2,3]") monkeypatch.setenv("SOME_LIMIT", "100") cfg = Config("example.production") assert cfg.API_KEY == "prod_key" assert cfg.SOME_LIMIT == 100 # Invalid types would be ignored assert cfg.DATABASE == {"host": "db.com", "user": "******"} # Correct types would be parsed monkeypatch.setenv("DATABASE", '{"host": "new.com", "user": "******"}') cfg = Config("example.production") assert cfg.DATABASE == {"host": "new.com", "user": "******"} cfg = Config("example.defaults") assert cfg.DATABASE == {"host": "new.com", "user": "******"} monkeypatch.setenv("APP_SECRET", "value_from_env_with_prefix") monkeypatch.setenv("DEMOENV", "ignore_me") cfg = Config("example.production", config_config=dict(env_prefix="APP_")) assert cfg.SECRET == "value_from_env_with_prefix" assert cfg.ENV == "production"
def __init__(self, app: Application = None, **options): """Save application and create he plugin's configuration.""" self.cfg = Config(config_config={'update_from_env': False}, **self.defaults) if app is not None: self.setup(app, **options) else: self.cfg.update_from_dict(options, exist_only=True)
def test_import_modules(): from example import tests from modconfig import Config # Config accepts modules themself cfg = Config(tests) assert cfg assert cfg.SECRET == "unsecure" assert cfg.LIMIT is None assert cfg.API_TOKEN_EXPIRE # Config accepts modules import path cfg = Config("example.tests", API_KEY="redefined") assert cfg assert cfg.API_KEY == "redefined" assert cfg.APP_DIR assert cfg.ENV == "tests" assert cfg.LIMIT is None assert cfg.SECRET == "unsecure" cfg.update_from_dict({"limit": "0.22"}) assert cfg.LIMIT == 0.22 mod = cfg.update_from_modules("example.unknown") assert not mod # If the first given module is not available then next would be used. mod = cfg.update_from_modules( "example.unknown", "example.tests", "example.production" ) assert mod == "example.tests" assert cfg.ENV == "tests"
def __init__(self, *cfg_mods: t.Union[str, ModuleType], **options): """Initialize the application. :param *cfg_mods: modules to import application's config :param **options: Configuration options """ from .plugins import BasePlugin self.plugins: t.Dict[str, BasePlugin] = dict() # Setup the configuration self.cfg = Config(**self.defaults, config_config=dict(update_from_env=False)) options['CONFIG'] = self.cfg.update_from_modules(*cfg_mods, 'env:%s' % CONFIG_ENV_VARIABLE) self.cfg.update(**options) self.cfg.update_from_env(prefix=f"{ self.cfg.name }_") # Setup CLI from .manage import Manager self.manage = Manager(self) # Setup logging LOG_CONFIG = self.cfg.get('LOG_CONFIG') if LOG_CONFIG and isinstance(LOG_CONFIG, dict) and LOG_CONFIG.get('version'): logging.config.dictConfig(LOG_CONFIG) # type: ignore self.logger = logging.getLogger('muffin') self.logger.setLevel(self.cfg.LOG_LEVEL) self.logger.propagate = False if not self.logger.handlers: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( self.cfg.LOG_FORMAT, self.cfg.LOG_DATE_FORMAT)) self.logger.addHandler(ch) super(Application, self).__init__( debug=self.cfg.DEBUG, logger=self.logger, trim_last_slash=self.cfg.TRIM_LAST_SLASH, static_folders=self.cfg.STATIC_FOLDERS, static_url_prefix=self.cfg.STATIC_URL_PREFIX, )
def test_update_from_dict(): from modconfig import Config cfg = Config(ignore_case=True, var1=1, VAR2=2) cfg.update_from_dict({"CFG_VAR1": 11, "CFG_VAR3": 33}, prefix="CFG_") assert cfg.var1 == 11 assert cfg.VAR2 == 2 with pytest.raises(AttributeError): assert cfg.var3 cfg = Config(VAR=42) cfg.update_from_dict({"CFG_VAR": 11, "VAR": 22}, prefix="CFG_") assert cfg.var == 11
def test_base(): from modconfig import Config cfg = Config("unknown", config_config=dict(env_prefix="CONFIG")) assert cfg assert cfg._Config__env_prefix == "CONFIG" with pytest.raises(AttributeError): cfg.UNKNOWN cfg = Config(OPTION=42) assert cfg assert cfg.OPTION == 42 assert cfg.get("OPTION") == 42 assert cfg.get("UNKNOWN", True) cfg.update(OPTION=43) assert cfg.OPTION == 43 test = cfg.update_from_modules() assert not test test = list(cfg) assert test == [("OPTION", 43)] cfg.update(fn=lambda: 42) assert cfg.FN() == 42
import logging from modconfig import Config __version__ = "1.2.0" # Configuration config: Config = Config( DEBUG=False, SCHEME='https', MAX_REDIRECTS=10, STATUS_URL='/knocker/status', HOSTS_ONLY=[], TIMEOUT=10.0, TIMEOUT_MAX=60.0, SENTRY_DSN='', SENTRY_FAILED_REQUESTS=False, RETRIES=2, RETRIES_MAX=10, RETRIES_BACKOFF_FACTOR=0.5, RETRIES_BACKOFF_FACTOR_MAX=600, LOG_FILE='-', LOG_LEVEL='INFO', LOG_FORMAT='[%(asctime)s] [%(process)s] [%(levelname)s] %(message)s', ) # Setup logging logger: logging.Logger = logging.getLogger('knocker') logger.setLevel(config.LOG_LEVEL) logger.propagate = False if config.LOG_FILE: handler = logging.StreamHandler()
class BasePlugin(ABC): """Base class for Muffin plugins.""" app: t.Optional[Application] = None # Plugin options with default values defaults: t.Dict[str, t.Any] = {} # Optional middleware method middleware: t.Optional[t.Callable] = None # Optional startup method startup: t.Optional[t.Callable] = None # Optional shutdown method shutdown: t.Optional[t.Callable] = None # Optional conftest method conftest: t.Optional[t.Callable] = None @property @abstractmethod def name(self): """Plugin has to have a name.""" raise NotImplementedError def __init__(self, app: Application = None, **options): """Save application and create he plugin's configuration.""" self.cfg = Config(config_config={'update_from_env': False}, **self.defaults) if app is not None: self.setup(app, **options) else: self.cfg.update_from_dict(options, exist_only=True) def __repr__(self) -> str: """Human readable representation.""" return f"<muffin.Plugin: { self.name }>" @property def installed(self): """Check the plugin is installed to an app.""" return bool(self.app) def setup(self, app: Application, *, name: str = None, **options): """Bind app and update the plugin's configuration.""" # allow to redefine the name for multi plugins with same type self.name = name or self.name # type: ignore self.app = app self.app.plugins[self.name] = self # Update configuration self.cfg.update_from_dict(dict(app.cfg), prefix=f"{self.name}_", exist_only=True) self.cfg.update_from_dict(options) # Register a middleware if self.middleware: self.app.middleware(to_awaitable(self.middleware)) # Bind startup if self.startup: self.app.on_startup(self.startup) # Bind shutdown if self.shutdown: self.app.on_shutdown(self.shutdown)
class Application(BaseApp): """The Muffin Application.""" # Default configuration values defaults: t.Dict = dict( # The application's name NAME='muffin', # Path to configuration module CONFIG=None, # Enable debug mode (optional) DEBUG=False, # Routing options TRIM_LAST_SLASH=True, # Static files options STATIC_URL_PREFIX='/static', STATIC_FOLDERS=[], # Logging options LOG_LEVEL='WARNING', LOG_FORMAT='%(asctime)s [%(process)d] [%(levelname)s] %(message)s', LOG_DATE_FORMAT='[%Y-%m-%d %H:%M:%S]', LOG_CONFIG=None, ) def __init__(self, *cfg_mods: t.Union[str, ModuleType], **options): """Initialize the application. :param *cfg_mods: modules to import application's config :param **options: Configuration options """ from .plugins import BasePlugin self.plugins: t.Dict[str, BasePlugin] = dict() # Setup the configuration self.cfg = Config(**self.defaults, config_config=dict(update_from_env=False)) options['CONFIG'] = self.cfg.update_from_modules(*cfg_mods, 'env:%s' % CONFIG_ENV_VARIABLE) self.cfg.update(**options) self.cfg.update_from_env(prefix=f"{ self.cfg.name }_") # Setup CLI from .manage import Manager self.manage = Manager(self) # Setup logging LOG_CONFIG = self.cfg.get('LOG_CONFIG') if LOG_CONFIG and isinstance(LOG_CONFIG, dict) and LOG_CONFIG.get('version'): logging.config.dictConfig(LOG_CONFIG) # type: ignore self.logger = logging.getLogger('muffin') self.logger.setLevel(self.cfg.LOG_LEVEL) self.logger.propagate = False if not self.logger.handlers: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( self.cfg.LOG_FORMAT, self.cfg.LOG_DATE_FORMAT)) self.logger.addHandler(ch) super(Application, self).__init__( debug=self.cfg.DEBUG, logger=self.logger, trim_last_slash=self.cfg.TRIM_LAST_SLASH, static_folders=self.cfg.STATIC_FOLDERS, static_url_prefix=self.cfg.STATIC_URL_PREFIX, ) def __repr__(self) -> str: """Human readable representation.""" return f"<muffin.Application: { self.cfg.name }>" def import_submodules(self, *submodules: str) -> t.Dict[str, ModuleType]: """Import application components.""" parent_frame = inspect.stack()[1][0] package_name = parent_frame.f_locals['__name__'] return import_submodules(package_name, *submodules)