__copyright__ = "Copyright (c) 2020-2024 Alex Laird"
__license__ = "MIT"
import os
import click
import confuse
from hookee.exception import HookeeConfigError
template = {
"port": int,
"subdomain": confuse.String(default=None),
"region": confuse.Choice(["us", "eu", "ap", "au", "sa", "jp", "in", "us-cal-1"], default=None),
"domain": confuse.String(default=None),
# Deprecated, use "domain" instead
"hostname": confuse.String(default=None),
"basic_auth": confuse.String(default=None),
# Deprecated, use "basic_auth" instead
"auth": confuse.String(default=None),
"host_header": confuse.String(default=None),
"response": confuse.String(default=None),
"content_type": confuse.String(default=None),
"request_script": confuse.Filename(default=None),
"response_script": confuse.Filename(default=None),
"auth_token": confuse.String(default=os.environ.get("NGROK_AUTHTOKEN")),
"plugins_dir": confuse.Filename(),
"plugins": list,
"console_width": confuse.Integer(default=80),
"header_color": confuse.Integer(default="green"),
"default_color": confuse.Integer(default="white"),
"request_color": confuse.Integer(default="white"),
}
[docs]
class Config:
"""
An object with accessor methods containing ``hookee``'s configuration. Default configuration can be
overridden by creating a custom config at ``~/.config/hookee/config.yaml`` (when setting config
values from the command line, this is where updated values are stored) which in turn can be overridden by
passing args to the CLI.
If instantiating for a custom integration, args that would otherwise have been passed to and validated by the CLI
(see ``hookee --help``) can instead be passed as ``kwargs`` here to ensure the same validation is done.
For example:
.. code-block:: python
from hookee.conf import Config
config = Config(subdomain="my_domain",
region="eu")
A callback function can also be passed instead of ``response`` and ``content-type`` (or needing to use
plugins) when integrating with ``hookee``'s APIs:
.. code-block:: python
from hookee.conf import Config
def response_callback(request, response):
response.data = "<Response>Ok</Response>"
response.headers["Content-Type"] = "application/xml"
return response
config = Config(response_callback=response_callback)
:var config_obj: The templated config object.
:vartype config_obj: confuse.core.Configuration
:var config_dir: The directory of the config being used.
:vartype config_dir: str
:var config_path: The full path to the config file being used.
:vartype config_path: str
:var config_data: The parsed and validated config data. Use :func:`get`, :func:`set`, and other accessors
to interact with the data.
:vartype config_data: confuse.templates.AttrDict
:var click_logging: ``True`` if ``click`` should be used for log output, which enables colors and formatting when
logging to a console, ``False`` if a logger should be used. If not passed, ``True`` if a :class:`click.Context`
is found to be active. Not persisted to the config file.
:vartype click_logging: bool
:var response_callback: The response callback function, if defined. Not persisted to the config file.
:vartype response_callback: types.FunctionType, optional
"""
def __init__(self, click_logging=None, **kwargs):
try:
if click_logging is None:
click_logging = click.get_current_context(silent=True) is not None
self.response_callback = kwargs.pop("response_callback", None)
config = confuse.Configuration("hookee", __name__)
config.set_args(kwargs)
self.config_obj = config
self.config_dir = self.config_obj.config_dir()
self.config_path = os.path.join(self.config_dir, confuse.CONFIG_FILENAME)
self.config_data = config.get(template)
self.click_logging = click_logging
if self.config_data.get("response") and self.response_callback:
raise HookeeConfigError("Can't define both \"response\" and \"response_callback\".")
elif self.response_callback and not callable(self.response_callback):
raise HookeeConfigError("\"response_callback\" must be a function.")
plugins_dir = os.path.expanduser(self.config_data["plugins_dir"])
if not os.path.exists(plugins_dir):
os.makedirs(plugins_dir)
except confuse.NotFoundError as e:
raise HookeeConfigError(f"The config file is invalid: {str(e)}.")
except (confuse.ConfigReadError, ValueError):
raise HookeeConfigError("The config file is not valid YAML.")
[docs]
def get(self, key, default=None):
"""
Get the config value for the given key of persisted data.
:param key: The key.
:type key: str
:param default: The default, if config not set.
:type key: str
:return: The config value.
:rtype: object
"""
return self.config_data.get(key, default)
[docs]
def set(self, key, value):
"""
Update the config key to the given value, persisting to ``config.yaml``.
:param key: The key.
:type key: str
:param value: The value to set.
:type key: object
"""
if value != self.config_data[key]:
self._update_config_objects(key, value)
[docs]
def append(self, key, value):
"""
Update the config key by appending to the list the given value, persisting to ``config.yaml``.
:param key: The key.
:type key: str
:param value: The value to append.
:type value: object
"""
list_item = list(self.config_data[key])
if value not in list_item:
list_item.append(value)
self._update_config_objects(key, list_item)
[docs]
def remove(self, key, value):
"""
Update the config key by removing from the list the given value from the list for the given key, persisting to
``config.yaml``.
:param key: The key.
:type key: str
:param value: The value to remove.
:type value: object
"""
list_item = list(self.config_data[key])
if value in list_item:
list_item.remove(value)
self._update_config_objects(key, list_item)
def _update_config_objects(self, key, value):
self.config_data[key] = value
self.config_obj[key] = value
self._write_config_objects_to_file()
def _write_config_objects_to_file(self):
with open(self.config_path, "w") as f:
f.write(self.config_obj.dump())