From 91354ad3b94dba81f74a1b9283c24f69dd0c7755 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 5 Feb 2020 19:14:20 -0800 Subject: [PATCH 1/6] Move server from example to servelocal It's a neat utility --- example/glados_standalone/server.py => src/glados/servelocal.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename example/glados_standalone/server.py => src/glados/servelocal.py (100%) diff --git a/example/glados_standalone/server.py b/src/glados/servelocal.py similarity index 100% rename from example/glados_standalone/server.py rename to src/glados/servelocal.py From b70d534abfff84a567c4684ee7a3b4e65610da83 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 5 Feb 2020 19:14:38 -0800 Subject: [PATCH 2/6] Remove ngrok from example --- example/example_ngrok.py | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 example/example_ngrok.py diff --git a/example/example_ngrok.py b/example/example_ngrok.py deleted file mode 100644 index 2c98a76..0000000 --- a/example/example_ngrok.py +++ /dev/null @@ -1,8 +0,0 @@ -from pyngrok import ngrok -from example import FLASK_PORT - - -def start_ngrok(port=FLASK_PORT, options: dict = None): - if not options: - options = dict() - ngrok.connect(port=port, proto="http", options=options) From 922dd172157f138fcd0d1193f9b7153969ad748d Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 5 Feb 2020 19:16:50 -0800 Subject: [PATCH 3/6] Use relative imports instead of absolute This way we can avoid a bunch of circular imports and stop doing stuff like only importing certain things if we are doing type checking. --- src/glados/__init__.py | 22 +++++++--------- src/glados/bot.py | 23 +++++++++++------ src/glados/configs.py | 13 ++++++---- src/glados/core.py | 25 +++++++++--------- src/glados/plugin.py | 57 +++++++++++++++++++++++++++--------------- src/glados/request.py | 7 +++++- src/glados/router.py | 15 ++++++++--- src/glados/utils.py | 7 +++++- 8 files changed, 106 insertions(+), 63 deletions(-) diff --git a/src/glados/__init__.py b/src/glados/__init__.py index dcf468f..1a7a5f2 100644 --- a/src/glados/__init__.py +++ b/src/glados/__init__.py @@ -1,23 +1,19 @@ import logging -from .utils import PyJSON, get_var, get_enc_var -from .route_type import RouteType, EventRoutes, BOT_ROUTES, VERIFY_ROUTES -from .request import GladosRequest, SlackVerification +from .bot import BotImporter, GladosBot +from .configs import GladosConfig, read_config +from .core import Glados from .errors import ( - GladosPathExistsError, - GladosRouteNotFoundError, GladosBotNotFoundError, GladosError, + GladosPathExistsError, + GladosRouteNotFoundError, ) - -from .bot import GladosBot, BotImporter -from .router import GladosRouter, GladosRoute from .plugin import GladosPlugin, PluginImporter - -from .configs import GladosConfig -from .utils import read_config - -from .core import Glados +from .request import GladosRequest, SlackVerification +from .route_type import BOT_ROUTES, VERIFY_ROUTES, EventRoutes, RouteType +from .router import GladosRoute, GladosRouter +from .utils import PyJSON, get_enc_var, get_var # LOGGING_FORMAT = "%(asctime)s :: %(levelname)-8s :: [%(filename)s:%(lineno)s :: %(funcName)s() ] %(message)s" LOGGING_FORMAT = ( diff --git a/src/glados/bot.py b/src/glados/bot.py index 5904a92..8ec8f49 100644 --- a/src/glados/bot.py +++ b/src/glados/bot.py @@ -1,14 +1,15 @@ -from slack import WebClient -from slack.web.classes.messages import Message -from slack.web.slack_response import SlackResponse -from slack.errors import SlackRequestError -import yaml import glob +import logging from typing import Dict, Union -import logging +import yaml +from slack import WebClient +from slack.errors import SlackRequestError +from slack.web.classes.messages import Message +from slack.web.slack_response import SlackResponse -from glados import GladosRequest, get_var, get_enc_var +from .request import GladosRequest +from .utils import get_enc_var, get_var class BotImporter: @@ -27,6 +28,7 @@ def import_bots(self): """ files = glob.glob(f"{self._dir}/*.yaml") logging.debug(f"bot config files found: {files}") + for f in files: with open(f) as file: self._bots_yaml.update(yaml.load(file, Loader=yaml.FullLoader)) @@ -92,18 +94,21 @@ def check_for_env_vars(self, value): Any: Returns the value of the var from either the passed in value, or the env var value. """ + if type(value) is dict and "env_var" in value: var_name = value["env_var"] try: return get_var(var_name) except KeyError: logging.critical(f"missing env var: {value['env_var']}") + if type(value) is dict and "enc_env_var" in value: var_name = value["enc_env_var"] try: return get_enc_var(var_name) except KeyError: logging.critical(f"missing enc env var: {value['enc_env_var']}") + return value def validate_slack_signature(self, request: GladosRequest): @@ -111,6 +116,7 @@ def validate_slack_signature(self, request: GladosRequest): signing_secret=self.signing_secret, **request.slack_verify.json ) logging.info(f"valid payload signature from slack: {valid}") + if not valid: raise SlackRequestError("Signature of request is not valid") @@ -128,6 +134,7 @@ def send_message(self, channel: str, message: Message) -> SlackResponse: ------- """ + return self.client.chat_postMessage( channel=channel, as_user=True, **message.to_dict() ).data @@ -145,6 +152,7 @@ def update_message(self, channel: str, ts: str, message: Message) -> SlackRespon ------- """ + return self.client.chat_update(channel=channel, ts=ts, **message.to_dict()).data def delete_message(self, channel: str, ts: str) -> SlackResponse: @@ -159,4 +167,5 @@ def delete_message(self, channel: str, ts: str) -> SlackResponse: ------- """ + return self.client.chat_delete(channel=channel, ts=ts).data diff --git a/src/glados/configs.py b/src/glados/configs.py index 024ab11..73df5f8 100644 --- a/src/glados/configs.py +++ b/src/glados/configs.py @@ -1,10 +1,11 @@ import json -import yaml -from pathlib import Path -from glados import PyJSON import logging -from typing import Union, List +from pathlib import Path +from typing import List, Union + +import yaml +from .utils import PyJSON class GladosConfig: def __init__(self, config_file: str): @@ -35,12 +36,14 @@ def read_config(self): @property def sections(self) -> List[str]: """what sections are there in the config file - + Returns ------- List[str]: sorted list of sections in the yaml file """ + if not self.config: return list() + return sorted(list(self.config.to_dict().keys())) diff --git a/src/glados/core.py b/src/glados/core.py index 1da9aae..3258559 100644 --- a/src/glados/core.py +++ b/src/glados/core.py @@ -1,19 +1,13 @@ -from typing import List, Dict, TYPE_CHECKING -import yaml import logging +from typing import Dict, List -from glados import ( - GladosPlugin, - GladosRequest, - GladosRouter, - GladosBot, - BotImporter, - PluginImporter, - read_config, -) +import yaml -if TYPE_CHECKING: - from glados import GladosConfig +from .bot import BotImporter, GladosBot +from .configs import GladosConfig, read_config +from .plugin import GladosPlugin, PluginImporter +from .request import GladosRequest +from .router import GladosRouter class Glados: @@ -40,6 +34,7 @@ def __init__( def read_config(self): # TODO: Fix logging setup + if not self.config_file: logging.info("glados config file not set.") @@ -63,11 +58,13 @@ def read_config(self): self.bots_config_dir = config.get("bots_config_folder") import_bots = config.get("import_bots") + if import_bots: logging.info("auto-importing bots as set in glados config file") self.import_bots() import_plugins = config.get("import_plugins", True) + if import_plugins: self.import_plugins() @@ -86,6 +83,7 @@ def import_plugins(self): importer.discover_plugins() importer.load_discovered_plugins_config(False) importer.import_discovered_plugins(self.bots) + for plugin in importer.plugins.values(): print(type(plugin)) self.add_plugin(plugin) @@ -133,4 +131,5 @@ def request(self, request: GladosRequest): ------- """ + return self.router.exec_route(request) diff --git a/src/glados/plugin.py b/src/glados/plugin.py index 2f12a61..c126c1d 100644 --- a/src/glados/plugin.py +++ b/src/glados/plugin.py @@ -1,28 +1,19 @@ -from typing import Callable, Dict, Union -import yaml import glob +import importlib import logging -import requests - from pathlib import Path +from typing import Callable, Dict, Union -import importlib - -from glados import ( - GladosBot, - RouteType, - GladosRoute, - BOT_ROUTES, - GladosPathExistsError, - GladosBotNotFoundError, - GladosError, - GladosRequest, - VERIFY_ROUTES, - EventRoutes, -) - +import requests +import yaml from slack.web.classes.messages import Message -from slack.web.classes.objects import MarkdownTextObject, TextObject, PlainTextObject +from slack.web.classes.objects import MarkdownTextObject, PlainTextObject, TextObject + +from .bot import GladosBot +from .errors import GladosBotNotFoundError, GladosError, GladosPathExistsError +from .request import GladosRequest +from .route_type import BOT_ROUTES, VERIFY_ROUTES, EventRoutes, RouteType +from .router import GladosRoute SLACK_MESSAGE_TYPES = [Message, MarkdownTextObject, TextObject, PlainTextObject] @@ -72,6 +63,7 @@ def update(self, config: "PluginConfig", use_base_module: bool = True): """ config = config.__dict__.copy() self_config = self.__dict__ + if use_base_module: self_config.pop("module") self_config.pop("package") @@ -80,8 +72,10 @@ def update(self, config: "PluginConfig", use_base_module: bool = True): def to_dict(self, user_config_only=True): config = dict(enabled=self.enabled, bot=self.bot.to_dict()) + if not user_config_only: config["module"] = self.module + return {self.name: config} def to_yaml(self, user_config_only=True): @@ -113,15 +107,18 @@ def load_discovered_plugins_config(self, write_to_user_config=True): plugin_user_config = None logging.debug("starting import of plugins") + for config_file in self.config_files: # Read the plugin package config plugin_name = None with open(config_file) as file: c = yaml.load(file, yaml.FullLoader) + if len(c.keys()) != 1: logging.critical( f"zero or more than one object in config file: {config_file}" ) + continue plugin_name = list(c.keys())[0] c[plugin_name]["config_file"] = config_file @@ -131,11 +128,13 @@ def load_discovered_plugins_config(self, write_to_user_config=True): logging.critical( f"invalid or missing plugin name. config file: {config_file}" ) + continue user_config_path = Path(self.plugins_config_folder, f"{plugin_name}.yaml") # Write defaults to user file + if not user_config_path.is_file() and write_to_user_config: with open(user_config_path, "w") as file: plugin_package_config.enabled = False @@ -143,14 +142,17 @@ def load_discovered_plugins_config(self, write_to_user_config=True): elif not user_config_path.is_file() and not write_to_user_config: logging.warning(f"no user plugin config for {plugin_name}. skipping.") + continue with open(user_config_path) as file: c = yaml.load(file, yaml.FullLoader) + if len(c.keys()) != 1: logging.critical( f"zero or more than one object in config file: {config_file}" ) + continue c[plugin_name]["config_file"] = str(user_config_path) plugin_user_config = PluginConfig(plugin_name, **c[plugin_name]) @@ -172,9 +174,11 @@ def import_discovered_plugins(self, bots: Dict[str, GladosBot]): the results are updated in self.plugins """ + for plugin_name, plugin_config in self.plugin_configs.items(): if not plugin_config.enabled: logging.warning(f"plugin {plugin_name} is disabled") + continue logging.info(f"importing plugin: {plugin_name}") module = importlib.import_module(plugin_config.package) @@ -187,6 +191,7 @@ def get_required_bot( if not bot_name: raise GladosError(f"no bot name set for plugin: {plugin_name}") bot = bots.get(bot_name) + if not bot: logging.error( f"bot: {bot_name} is not found. disabling plugin: {plugin_name}" @@ -194,6 +199,7 @@ def get_required_bot( raise GladosBotNotFoundError( f"bot: {bot_name} is not found as required for {plugin_name}" ) + return bot try: @@ -201,6 +207,7 @@ def get_required_bot( except GladosError as e: logging.error(f"{e} :: disabling plugin: {plugin_name}") self.plugin_configs[plugin_name]["enabled"] = False + continue plugin = getattr(module, plugin_config.module)(bot, plugin_name) @@ -247,11 +254,14 @@ def add_route( ------- """ + if type(route) is EventRoutes: route = route.name new_route = GladosRoute(route_type, route, function) + if route_type in BOT_ROUTES: new_route.route = f"{self.bot.name}_{route}" + if new_route.route in self._routes[new_route.route_type.value]: raise GladosPathExistsError( f"a route with the name of {new_route.route} already exists in the route type: {new_route.route_type.name}" @@ -274,20 +284,25 @@ def send_request(self, request: GladosRequest, **kwargs): ------- """ + if request.route_type in VERIFY_ROUTES: self.bot.validate_slack_signature(request) response = self._routes[request.route_type.value][request.route].function( request, **kwargs ) + if response is None: # TODO(zpriddy): add logging. + return "" if request.route_type is RouteType.Interaction and request.response_url: if type(response) is str: self.respond_to_url(request, response) + if type(response) in SLACK_MESSAGE_TYPES: response = response.to_dict() + if type(response) is dict: self.respond_to_url(request, **response) @@ -296,6 +311,7 @@ def send_request(self, request: GladosRequest, **kwargs): def respond_to_url(self, request: GladosRequest, text: str, **kwargs): if not request.response_url: logging.error("no response_url provided in request.") + return kwargs["text"] = text r = requests.post(request.response_url, json=kwargs) @@ -317,4 +333,5 @@ def routes(self): for route in [route_type for route_type in self._routes.values()] ] ] + return routes diff --git a/src/glados/request.py b/src/glados/request.py index 13dfd3a..3629887 100644 --- a/src/glados/request.py +++ b/src/glados/request.py @@ -1,6 +1,8 @@ -from glados import RouteType, BOT_ROUTES, PyJSON from typing import Union +from .route_type import BOT_ROUTES, RouteType +from .utils import PyJSON + class SlackVerification: """An object to hold slack verification data @@ -85,8 +87,10 @@ def __init__( if route_type is RouteType.Menu: self._route = self.json.action_id + if route_type is RouteType.Interaction: self._route = self.json.actions[0].action_id + if route_type is RouteType.Events: self._route = self.json.event.type @@ -96,6 +100,7 @@ def route(self) -> str: If the route automatically prefixed the route with the bot name, it will return the route with the prefix """ + return ( f"{self.bot_name}_{self._route}" if self.route_type in BOT_ROUTES diff --git a/src/glados/router.py b/src/glados/router.py index 0768d2f..6385964 100644 --- a/src/glados/router.py +++ b/src/glados/router.py @@ -1,10 +1,12 @@ import logging -from typing import Callable, Dict, List, NoReturn, Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, Dict, NoReturn, Optional -from glados import GladosRequest, GladosRouteNotFoundError, RouteType +from .errors import GladosRouteNotFoundError +from .request import GladosRequest +from .route_type import RouteType if TYPE_CHECKING: - from glados import GladosPlugin + from .plugin import GladosPlugin class GladosRoute(object): @@ -23,6 +25,7 @@ class GladosRouter(object): def __init__(self, **kwargs): # routes are stored as: {RouteType.SendMessage: {"ask_user",ask_user, "confirm":confirm}} self.routes = dict() # type: Dict[RouteType, Dict[str, GladosRoute]] + for route in RouteType._member_names_: self.routes[RouteType[route].value] = dict() # type: Dict[str, GladosRoute] @@ -43,6 +46,7 @@ def add_route(self, plugin: "GladosPlugin", route: GladosRoute) -> NoReturn: """ logging.debug(f"adding route: {route}") + if route.route in self.routes[route.route_type.value]: raise KeyError( f"a route with the name of {route.route} already exists in the route type: {route.route_type.name}" @@ -61,6 +65,7 @@ def add_routes(self, plugin: "GladosPlugin"): ------- """ + for route in plugin.routes: self.add_route(plugin, route) @@ -84,10 +89,12 @@ def get_route(self, route_type: RouteType, route: str) -> Optional[GladosRoute]: GladosRouteNotFoundError the requested route is not found """ + if not self.routes[route_type.value].get(route): raise GladosRouteNotFoundError( f"no route with the name of {route} exists in route type: {route_type.name}" ) + return self.routes[route_type.value][route] def route_function(self, route_type: RouteType, route: str) -> Callable: @@ -106,6 +113,7 @@ def route_function(self, route_type: RouteType, route: str) -> Callable: return the requested routes callable function """ + return self.get_route(route_type, route) def exec_route(self, request: GladosRequest): @@ -146,4 +154,5 @@ def exec_route(self, request: GladosRequest): False """ logging.debug(f"calling route function for {request.route}") + return self.route_function(request.route_type, request.route)(request) diff --git a/src/glados/utils.py b/src/glados/utils.py index 774e1b6..98475de 100644 --- a/src/glados/utils.py +++ b/src/glados/utils.py @@ -1,12 +1,12 @@ import json import logging - import os from base64 import b64decode def get_var(var_name: str): """Get an ENV VAR""" + return os.environ[var_name] @@ -52,11 +52,14 @@ def __init__(self, d): def from_dict(self, d): self.__dict__ = {} + for key, value in d.items(): if type(value) is dict: value = PyJSON(value) + if type(value) is list: value_list = list() + for v in value: if type(v) in [list, dict]: value_list.append(PyJSON(v)) @@ -67,10 +70,12 @@ def from_dict(self, d): def to_dict(self): d = {} + for key, value in self.__dict__.items(): if type(value) is PyJSON: value = value.to_dict() d[key] = value + return d def __repr__(self): From baa3b139daf65f5144dfed79e80385a529e99643 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 5 Feb 2020 19:18:04 -0800 Subject: [PATCH 4/6] Move read_config to glados.configs instead --- src/glados/configs.py | 9 +++++++++ src/glados/utils.py | 10 +--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/glados/configs.py b/src/glados/configs.py index 73df5f8..0cfcd2f 100644 --- a/src/glados/configs.py +++ b/src/glados/configs.py @@ -7,6 +7,15 @@ from .utils import PyJSON + +def read_config(config_file: str): + logging.debug(f"Reading GLaDOS config from {config_file}") + config = GladosConfig(config_file) + config.read_config() + + return config + + class GladosConfig: def __init__(self, config_file: str): self.config = None # type: Union[None, PyJSON] diff --git a/src/glados/utils.py b/src/glados/utils.py index 98475de..cb1e548 100644 --- a/src/glados/utils.py +++ b/src/glados/utils.py @@ -31,16 +31,8 @@ def decode_kms(ciphertext_blob: str) -> str: def get_enc_var(var_name: str): """Get an encrypted ENV VAR""" ciphertext_blob = get_var(var_name) - return decode_kms(ciphertext_blob) - -def read_config(config_file: str): - from glados import GladosConfig - - logging.debug(f"Reading GLaDOS config from {config_file}") - config = GladosConfig(config_file) - config.read_config() - return config + return decode_kms(ciphertext_blob) class PyJSON: From ad9e2630d13e4c078f98cfb072346a6c1a116b2f Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 5 Feb 2020 19:20:57 -0800 Subject: [PATCH 5/6] Add servelocal command to GLaDOS --- setup.cfg | 4 +++ src/glados/servelocal.py | 64 +++++++++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3ab2ab8..796d7f8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,10 @@ install_requires = pyyaml requests +[options.entry_points] +console_scripts = + glados-servelocal = glados.servelocal:run + [options.packages.find] where=src diff --git a/src/glados/servelocal.py b/src/glados/servelocal.py index 65edd39..4edcb13 100644 --- a/src/glados/servelocal.py +++ b/src/glados/servelocal.py @@ -1,30 +1,22 @@ -from os import getenv +""" +Serves a local Flask application for GLaDOS bots/plugins +""" + +import argparse +import json import logging from flask import Flask, request -from glados import ( - Glados, - GladosBot, - GladosRequest, - RouteType, - SlackVerification, - GladosRouteNotFoundError, - read_config, -) -from test_plugin.test_plugin import TestPlugin -from example import FLASK_HOST, FLASK_PORT -import json - -glados_config_file = "glados_standalone/glados.yaml" -config = read_config(glados_config_file) +from .configs import read_config +from .core import Glados +from .errors import GladosRouteNotFoundError +from .request import GladosRequest, SlackVerification +from .route_type import RouteType +from .router import GladosRoute app = Flask(__name__) - -server_config = config.config.server -glados = Glados(config.config_file) - -app.secret_key = server_config.secret_key +glados = None def extract_slack_info(r: request): @@ -32,10 +24,12 @@ def extract_slack_info(r: request): data = r.get_data(as_text=True) timestamp = r.headers.get("X-Slack-Request-Timestamp") signature = r.headers.get("X-Slack-Signature") + return SlackVerification(data, timestamp, signature) except Exception as e: # If it makes it here, the request probably isn't from Slack. logging.error(e) + return None @@ -44,6 +38,7 @@ def send_message_route(bot, route): glados_request = GladosRequest( RouteType.SendMessage, route, bot_name=bot, json=request.get_json() ) + return glados.request(glados_request) @@ -51,6 +46,7 @@ def send_message_route(bot, route): def event_subscriptions(bot): body = request.json event_type = body.get("type", "") + if event_type == "url_verification": return body.get("challenge") @@ -74,6 +70,7 @@ def slash_command(bot, route): r = GladosRequest( RouteType.Slash, route, slack_info, bot_name=bot, json=request_json ) + return glados.request(r) @@ -89,6 +86,7 @@ def interaction(bot): return glados.request(r) except GladosRouteNotFoundError as e: logging.error(e) + return "not found" @@ -98,15 +96,33 @@ def external_menu(): request_json = request.form.to_dict() request_json = json.loads(request_json.get("payload")) r = GladosRequest(RouteType.Menu, slack_verify=slack_info, json=request_json) + return glados.request(r) -def start(): - glados.read_config() +parser = argparse.ArgumentParser( + description="Serve GLaDOS and plugins using a Flask server" +) +parser.add_argument( + "configfile", + type=str, + help="Use the provided configfile for starting the server/GLaDOS bots", +) def run(): - start() + global glados + + args = parser.parse_args() + configfile = args.configfile + + config = read_config(configfile) + + server_config = config.config.server + glados = Glados(config.config_file) + glados.read_config() + + app.secret_key = server_config.secret_key app.run(server_config.host, server_config.port, debug=server_config.debug) From 2871435113b1ad2ac8ba1efb0d7c4f15cd13d8fc Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Wed, 5 Feb 2020 19:35:43 -0800 Subject: [PATCH 6/6] Update example requirements --- example/requirements.txt | 9 +++------ setup.cfg | 4 ++++ src/glados/servelocal.py | 9 +++++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/example/requirements.txt b/example/requirements.txt index 48993ca..13b5362 100644 --- a/example/requirements.txt +++ b/example/requirements.txt @@ -1,7 +1,4 @@ -pyramid==1.10.4 -Flask -python-dotenv -blinker +glados[servelocal] -# Uncomment this line if you want to use ngrok -# pyngrok \ No newline at end of file +# Or if you want ngrok support: +# glados[servelocal,ngrok] diff --git a/setup.cfg b/setup.cfg index 796d7f8..5bbcfd0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,10 @@ docs = Sphinx sphinx-rtd-theme sphinx-autodoc-typehints +servelocal = + Flask +ngrok = + pyngrok [bdist_wheel] universal=0 diff --git a/src/glados/servelocal.py b/src/glados/servelocal.py index 4edcb13..1f5ca57 100644 --- a/src/glados/servelocal.py +++ b/src/glados/servelocal.py @@ -6,8 +6,6 @@ import json import logging -from flask import Flask, request - from .configs import read_config from .core import Glados from .errors import GladosRouteNotFoundError @@ -15,6 +13,13 @@ from .route_type import RouteType from .router import GladosRoute +try: + from flask import Flask, request +except ImportError: + raise ImportError( + "Flask is not installed, please install Flask or install glados extra 'servelocal'" + ) + app = Flask(__name__) glados = None