From b06de92562fe2b4dd112ac0024d49b62fa27110c Mon Sep 17 00:00:00 2001 From: Alex Laird Date: Wed, 16 Sep 2020 22:26:09 -0700 Subject: [PATCH] - If `hookee` was instantiated programmatically rather than from the CLI (i.e. `click.Context` is `None`), `HookeeManager` throws exceptions and `PrintUtil` appends to a logger instead of interacting with `click.Context` or `echo` functions. --- CHANGELOG.md | 7 +++--- docs/index.rst | 5 ++++ hookee/conf.py | 30 ++++++++++------------- hookee/hookeemanager.py | 16 +++++++------ hookee/pluginmanager.py | 7 ++---- hookee/tunnel.py | 3 ++- hookee/util.py | 38 +++++++++++++++++++----------- tests/managedtestcase.py | 1 + tests/test_cli.py | 2 +- tests/test_hookee_manager.py | 4 ++-- tests/test_hookee_manager_edges.py | 3 +++ tests/test_plugin_manager.py | 4 ++-- 12 files changed, 68 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dfdf04..1a7fef1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed - `PluginManager`'s `response_body` and `response_content_type` variables have been replaced with `response_callback`, a lambda that is generated if these configuration values are given. -- Removed `PluginManager`'s `request_script` and `response_script` variables, instead these are added to `loaded_plugins` after their `Plugin` is validated and instantiated. +- Removed `PluginManager`'s `request_script` and `response_script` variables, instead these are added to `loaded_plugins` after their `Plugin` is validated and instantiated. +- If `hookee` was instantiated programmatically rather than from the CLI (i.e. `click.Context` is `None`), `HookeeManager` throws exceptions and `PrintUtil` appends to a logger instead of interacting with `click.Context` or `echo` functions. ### Removed -- `conf.Context` in favor of using `click`'s own `Context` object. -- Access to the `click` `Context` except in `HookeeManager`, which now has its own abstraction around `Context`-related actions. +- `conf.Context` in favor of using `click.Context`. +- Access to the `click.Context` except in `HookeeManager`, which now has its own abstraction around such actions. ## [1.1.0](https://github.com/alexdlaird/hookee/compare/1.0.1...1.1.0) - 2019-09-15 ### Added diff --git a/docs/index.rst b/docs/index.rst index 9e9576b..6bafb26 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -192,6 +192,11 @@ Under the hood, ``hookee`` uses `Flask `_ to open and manage its tunnel. Being familiar with these two packages would allow ``hookee`` to be configured and extended further. +``hookee`` can also be instantiated programmatically instead of from the console. To integrate with ``hookee`` +this way, have a look at the :class:`~hookee.hookeemanager.HookeeManager` as a starting point. When instantiating +``hookee`` this way, be sure to configure logging, otherwise request data that would otherwise be output to the +console (for instance request data dumped from plugins) won't be seen. + For more advanced ``hookee`` usage, its own API documentation is also available. .. toctree:: diff --git a/hookee/conf.py b/hookee/conf.py index 6006f88..81f76d7 100644 --- a/hookee/conf.py +++ b/hookee/conf.py @@ -1,5 +1,6 @@ import os +import click import confuse __author__ = "Alex Laird" @@ -58,11 +59,16 @@ def response_callback(request, response): :vartype config_obj: confuse.core.Configuration :var config_dir: The directory of the config being used. :vartype config_dir: str - :var config_filename: The full path to the config file being used. - :vartype config_filename: 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_ctx: ``True`` if the app is running from a ``click`` CLI, ``False`` if it was started programmatically. + Not persisted to the config file. + :vartype click_ctx: 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, **kwargs): @@ -78,6 +84,8 @@ def __init__(self, **kwargs): self.config_data = config.get(template) + self.click_ctx = click.get_current_context(silent=True) is not None + 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): @@ -93,16 +101,13 @@ def __init__(self, **kwargs): def get(self, key): """ - Get the config value for the given key. + Get the config value for the given key of persisted data. :param key: The key. :type key: str :return: The config value. :rtype: object """ - if key == "response_callback": - return self.response_callback - return self.config_data[key] def set(self, key, value): @@ -114,17 +119,8 @@ def set(self, key, value): :param value: The value to set. :type key: object """ - if key == "response_callback": - print(value) - print(type(value)) - if not callable(value): - raise HookeeConfigError("\"response_callback\" must be a function.") - else: - self.response_callback = value - - else: - if value != self.config_data[key]: - self._update_config_objects(key, value) + if value != self.config_data[key]: + self._update_config_objects(key, value) def append(self, key, value): """ diff --git a/hookee/hookeemanager.py b/hookee/hookeemanager.py index 86735cd..9f894d2 100644 --- a/hookee/hookeemanager.py +++ b/hookee/hookeemanager.py @@ -65,10 +65,7 @@ def __init__(self, config=None, load_plugins=True): data = self.ctx.obj if self.ctx is not None else {} config = Config(**data) except HookeeConfigError as e: - if self.ctx is not None: - self.fail(str(e)) - else: - raise e + self.fail(str(e), e) self.config = config self.plugin_manager = PluginManager(self) @@ -124,6 +121,7 @@ def print_hookee_banner(self): |___| /\____/ \____/|__|_ \\___ >\___ > \/ \/ \/ \/ v{}""".format(__version__), fg="green", bold=True) + self.print_util.print_basic() self.print_util.print_close_header("=", blank_line=False) def print_ready(self): @@ -141,8 +139,8 @@ def print_ready(self): rules = list(filter(lambda r: r.rule not in ["/shutdown", "/static/", "/status"], self.server.app.url_map.iter_rules())) for rule in rules: - self.print_util.print_basic(" * {}{}".format(self.tunnel.public_url, rule.rule)) - self.print_util.print_basic(" Methods: {}".format(sorted(list(rule.methods)))) + self.print_util.print_basic(" * {}{}".format(self.tunnel.public_url, rule.rule), print_when_logging=True) + self.print_util.print_basic(" Methods: {}".format(sorted(list(rule.methods))), print_when_logging=True) self.print_util.print_close_header() @@ -150,16 +148,20 @@ def print_ready(self): self.print_util.print_basic("--> Ready, send a request to a registered endpoint ...", fg="green", bold=True) self.print_util.print_basic() - def fail(self, msg): + def fail(self, msg, e=None): """ Shutdown the curent application with a failure. If a CLI Context exists, that will be used to invoke the failure, otherwise an exception will be thrown for failures to be caught programmatically. :param msg: The failure message. :type msg: str + :param e: The error being raised. + :type e: HookeeError, optional """ if self.ctx is not None: self.ctx.fail(msg) + elif e: + raise e else: raise HookeeError(msg) diff --git a/hookee/pluginmanager.py b/hookee/pluginmanager.py index b36ffb7..b76f254 100644 --- a/hookee/pluginmanager.py +++ b/hookee/pluginmanager.py @@ -225,7 +225,7 @@ def load_plugins(self): if response_content_type and not response_body: self.hookee_manager.fail("If `--content-type` is given, `--response` must also be given.") - self.response_callback = self.config.get("response_callback") + self.response_callback = self.config.response_callback if self.response_callback and response_body: self.hookee_manager.fail("If `response_callback` is given, `response` cannot also be given.") @@ -310,10 +310,7 @@ def get_plugin(self, plugin_name): except ImportError: self.hookee_manager.fail("Plugin \"{}\" could not be found.".format(plugin_name)) except HookeePluginValidationError as e: - if self.hookee_manager.ctx is not None: - self.hookee_manager.fail(str(e)) - else: - raise e + self.hookee_manager.fail(str(e), e) def enabled_plugins(self): """ diff --git a/hookee/tunnel.py b/hookee/tunnel.py index 6a63ef6..0a468ae 100644 --- a/hookee/tunnel.py +++ b/hookee/tunnel.py @@ -113,5 +113,6 @@ def stop(self): def print_close_header(self): self.print_util.print_basic( - " * Tunnel: {} -> http://127.0.0.1:{}".format(self.public_url.replace("http://", "https://"), self.port)) + " * Tunnel: {} -> http://127.0.0.1:{}".format(self.public_url.replace("http://", "https://"), self.port), + print_when_logging=True) self.print_util.print_close_header() diff --git a/hookee/util.py b/hookee/util.py index c8f6257..4de3a45 100644 --- a/hookee/util.py +++ b/hookee/util.py @@ -1,5 +1,6 @@ import inspect import json +import logging import os import sys import xml.dom.minidom @@ -8,7 +9,9 @@ __author__ = "Alex Laird" __copyright__ = "Copyright 2020, Alex Laird" -__version__ = "1.1.0" +__version__ = "1.2.0" + +logger = logging.getLogger(__name__) class PrintUtil: @@ -27,7 +30,7 @@ def console_width(self): return self.config.get("console_width") def print_config_update(self, msg): - click.secho("\n--> {}\n".format(msg), fg="green") + self.print_basic("\n--> {}\n".format(msg), fg="green") def print_open_header(self, title, delimiter="-", fg="green"): """ @@ -42,9 +45,8 @@ def print_open_header(self, title, delimiter="-", fg="green"): """ width = int((self.console_width - len(title)) / 2) - click.echo("") - click.secho("{}{}{}".format(delimiter * width, title, delimiter * width), fg=fg, bold=True) - click.echo("") + self.print_basic() + self.print_basic("{}{}{}".format(delimiter * width, title, delimiter * width), fg=fg, bold=True) def print_close_header(self, delimiter="-", fg="green", blank_line=True): """ @@ -58,8 +60,8 @@ def print_close_header(self, delimiter="-", fg="green", blank_line=True): :type blank_line: bool """ if blank_line: - click.echo("") - click.secho(delimiter * self.console_width, fg=fg, bold=True) + self.print_basic() + self.print_basic(delimiter * self.console_width, fg=fg, bold=True) def print_dict(self, title, data, fg="green"): """ @@ -72,7 +74,7 @@ def print_dict(self, title, data, fg="green"): :param fg: The color to make the text. :type fg: str, optional """ - click.secho("{}: {}".format(title, json.dumps(data, indent=4)), fg=fg) + self.print_basic("{}: {}".format(title, json.dumps(data, indent=4)), fg=fg) def print_xml(self, title, data, fg="green"): """ @@ -85,20 +87,28 @@ def print_xml(self, title, data, fg="green"): :param fg: The color to make the text. :type fg: str, optional """ - click.secho("{}: {}".format(title, xml.dom.minidom.parseString(data).toprettyxml()), fg=fg) + self.print_basic("{}: {}".format(title, xml.dom.minidom.parseString(data).toprettyxml()), fg=fg) - def print_basic(self, data="", fg="white", bold=False): + def print_basic(self, msg="", fg="white", bold=False, print_when_logging=False): """ Print a status update in a boot sequence. - :param data: The update to print. - :type data: str, optional + :param msg: The update to print. + :type msg: str, optional :param fg: The color to make the text. :type fg: str, optional - :param bold: True if the output should be bold. + :param bold: ``True`` if the output should be bold. :type bold: bool, optional + :param print_when_logging: ``True`` if, when ``click_ctx`` is ``False``, ``msg`` should print to the console + instead appended to the logger. + :type print_when_logging: bool, optional """ - click.secho(data, fg=fg, bold=bold) + if self.config.click_ctx: + click.secho(msg, fg=fg, bold=bold) + elif not print_when_logging: + logger.info(msg) + else: + print(msg) def python3_gte(): diff --git a/tests/managedtestcase.py b/tests/managedtestcase.py index b53b636..514e9ba 100644 --- a/tests/managedtestcase.py +++ b/tests/managedtestcase.py @@ -31,6 +31,7 @@ def setUp(self): @classmethod def setUpClass(cls): cls.hookee_manager = HookeeManager() + cls.hookee_manager.config.click_ctx = True cls.hookee_manager._init_server_and_tunnel() diff --git a/tests/test_cli.py b/tests/test_cli.py index 7ef88a6..05e333d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,7 +8,7 @@ __author__ = "Alex Laird" __copyright__ = "Copyright 2020, Alex Laird" -__version__ = "1.1.0" +__version__ = "1.2.0" class TestCli(HookeeTestCase): diff --git a/tests/test_hookee_manager.py b/tests/test_hookee_manager.py index 20bf280..11832a2 100644 --- a/tests/test_hookee_manager.py +++ b/tests/test_hookee_manager.py @@ -5,7 +5,7 @@ __author__ = "Alex Laird" __copyright__ = "Copyright 2020, Alex Laird" -__version__ = "1.1.0" +__version__ = "1.2.0" class TestHookeeManager(ManagedTestCase): @@ -68,4 +68,4 @@ def test_http_post_json_data(self): "json_data_1": "json_data_value_1" }""", out.getvalue()) self.assertEqual(response.headers.get("Content-Type"), "application/json") - self.assertEqual(response.json(), data) \ No newline at end of file + self.assertEqual(response.json(), data) diff --git a/tests/test_hookee_manager_edges.py b/tests/test_hookee_manager_edges.py index 79e569a..70df151 100644 --- a/tests/test_hookee_manager_edges.py +++ b/tests/test_hookee_manager_edges.py @@ -12,6 +12,9 @@ class TestHookeeManagerEdges(HookeeTestCase): + def test_not_click_ctx(self): + self.assertFalse(self.config.click_ctx) + def test_hookee_manager(self): # GIVEN hookee_manager = HookeeManager() diff --git a/tests/test_plugin_manager.py b/tests/test_plugin_manager.py index cb0fa09..cffbcbd 100644 --- a/tests/test_plugin_manager.py +++ b/tests/test_plugin_manager.py @@ -88,7 +88,7 @@ def response_callback(request, response): return response self.assertEqual(0, len(self.plugin_manager.loaded_plugins)) - self.hookee_manager.config.set("response_callback", response_callback) + self.hookee_manager.config.response_callback = response_callback # WHEN self.plugin_manager.load_plugins() @@ -128,7 +128,7 @@ def test_load_plugins(self): self.assertTrue(hasattr(plugin.module, "plugin_type")) self.assertIn(plugin.module.plugin_type, VALID_PLUGIN_TYPES) self.assertTrue(hasattr(plugin.module, "setup")) - if not plugin.plugin_type != BLUEPRINT_PLUGIN: + if plugin.plugin_type != BLUEPRINT_PLUGIN: self.assertTrue(hasattr(plugin.module, "run")) if plugin.name == "custom_request_plugin":