From 8afbd3b65effaef9c80a4f0b2a304a48c6c2a62f Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Fri, 5 Aug 2022 22:45:35 +0200 Subject: [PATCH 1/2] add slack client and duneapi to base query monitor --- .pylintrc | 2 +- src/query_monitor/base.py | 47 +++++++++++++++++++++++---------------- src/slack_client.py | 34 ++++++++++++++++++++++++++++ src/slackbot.py | 18 +++++---------- 4 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 src/slack_client.py diff --git a/.pylintrc b/.pylintrc index b2481f6..e3c7327 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,2 +1,2 @@ [MASTER] -disable=fixme,logging-fstring-interpolation,too-many-arguments \ No newline at end of file +disable=fixme,logging-fstring-interpolation,too-many-arguments,too-few-public-methods \ No newline at end of file diff --git a/src/query_monitor/base.py b/src/query_monitor/base.py index 9fdc1c7..d30317b 100644 --- a/src/query_monitor/base.py +++ b/src/query_monitor/base.py @@ -9,7 +9,8 @@ from duneapi.api import DuneAPI from duneapi.types import QueryParameter, DuneRecord -from slack.web.client import WebClient + +from src.slack_client import BasicSlackClient log = logging.getLogger(__name__) logging.config.fileConfig(fname="logging.conf", disable_existing_loggers=False) @@ -28,22 +29,27 @@ def __init__( query_id: int, params: Optional[list[QueryParameter]] = None, threshold: int = 0, + # TODO - These useless trivial defaults are only temporary... I hope. + slack_client: BasicSlackClient = BasicSlackClient("", ""), + dune: DuneAPI = DuneAPI("", ""), ): self.query_id = query_id self.fixed_params = params if params else [] self.name = name # Threshold for alert worthy number of results. self.threshold = threshold + self.slack_client = slack_client + self.dune = dune def result_url(self) -> str: """Returns a link to query results excluding fixed parameters""" return f"https://dune.com/queries/{self.query_id}" - def refresh(self, dune: DuneAPI) -> list[DuneRecord]: + def refresh(self) -> list[DuneRecord]: """Executes dune query by ID, and fetches the results by job ID returned""" # TODO - this could probably live in the base duneapi library. - job_id = dune.execute(self.query_id, self.parameters()) - return dune.get_results(job_id) + job_id = self.dune.execute(self.query_id, self.parameters()) + return self.dune.get_results(job_id) def parameters(self) -> list[QueryParameter]: """ @@ -52,35 +58,38 @@ def parameters(self) -> list[QueryParameter]: """ return self.fixed_params - def alert_message(self, num_results: int) -> str: + def alert_message(self, results: list[dict[str, str]]) -> str: """ Default Alert message if not special implementation is provided. Says which query returned how many results along with a link to Dune. """ + num_results = len(results) return ( f"{self.name} - detected {num_results} cases. " f"Results available at {self.result_url()}" ) - def run_loop( - self, dune: DuneAPI, slack_client: WebClient, alert_channel: str + def alert_or_log( + self, alert_condition: bool, alert_message: str, log_message: str ) -> None: + """Post message if alert_condition is met, otherwise logs info.""" + if alert_condition: + log.info(alert_message) + self.slack_client.post(alert_message) + else: + log.info(log_message) + + def run_loop(self) -> None: """ Standard run-loop refreshing query, fetching results and alerting if necessary. """ log.info(f'Refreshing "{self.name}" query {self.result_url()}') - results = self.refresh(dune) - if len(results) > self.threshold: - log.error(self.alert_message(len(results))) - slack_client.chat_postMessage( - channel=alert_channel, - text=self.alert_message(len(results)), - # Do not show link preview! - # https://api.slack.com/reference/messaging/link-unfurling - unfurl_media=False, - ) - else: - log.info(f"No {self.name} detected") + results = self.refresh() + self.alert_or_log( + alert_condition=len(results) > self.threshold, + alert_message=self.alert_message(results), + log_message=f"No {self.name} detected", + ) class QueryMonitor(BaseQueryMonitor): diff --git a/src/slack_client.py b/src/slack_client.py new file mode 100644 index 0000000..80c2b94 --- /dev/null +++ b/src/slack_client.py @@ -0,0 +1,34 @@ +""" +Since channel is fixed at the beginning and nobody wants to see unfurled media +(especially in an alert), this tiny class encapsulates a few things that would +otherwise be unnecessarily repeated. +""" +import ssl + +import certifi +from slack.web.client import WebClient + + +class BasicSlackClient: + """ + Basic Slack Client with message post functionality + constructed from an API token and channel + """ + + def __init__(self, token: str, channel: str) -> None: + self.client = WebClient( + token=token, + # https://stackoverflow.com/questions/59808346/python-3-slack-client-ssl-sslcertverificationerror + ssl=ssl.create_default_context(cafile=certifi.where()), + ) + self.channel = channel + + def post(self, message: str) -> None: + """Posts `message` to `self.channel` excluding link previews.""" + self.client.chat_postMessage( + channel=self.channel, + text=message, + # Do not show link preview! + # https://api.slack.com/reference/messaging/link-unfurling + unfurl_media=False, + ) diff --git a/src/slackbot.py b/src/slackbot.py index ecadc9b..b15501b 100644 --- a/src/slackbot.py +++ b/src/slackbot.py @@ -3,14 +3,12 @@ """ import argparse import os -import ssl -import certifi import dotenv from duneapi.api import DuneAPI -from slack.web.client import WebClient from src.query_monitor.factory import load_from_config +from src.slack_client import BasicSlackClient if __name__ == "__main__": parser = argparse.ArgumentParser("Missing Tokens") @@ -24,13 +22,9 @@ dotenv.load_dotenv() query_monitor = load_from_config(args.query_config) - - query_monitor.run_loop( - dune=DuneAPI.new_from_environment(), - slack_client=WebClient( - token=os.environ["SLACK_TOKEN"], - # https://stackoverflow.com/questions/59808346/python-3-slack-client-ssl-sslcertverificationerror - ssl=ssl.create_default_context(cafile=certifi.where()), - ), - alert_channel=os.environ["SLACK_ALERT_CHANNEL"], + # Set the third party communications to non-trivial after config instantiation + query_monitor.slack_client = BasicSlackClient( + token=os.environ["SLACK_TOKEN"], channel=os.environ["SLACK_ALERT_CHANNEL"] ) + query_monitor.dune = DuneAPI.new_from_environment() + query_monitor.run_loop() From 54229c3364c13b11ec35a59ea7f076722011c3cd Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Fri, 5 Aug 2022 22:48:12 +0200 Subject: [PATCH 2/2] fix test --- tests/test_implementations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_implementations.py b/tests/test_implementations.py index 09ced2e..7ab89c7 100644 --- a/tests/test_implementations.py +++ b/tests/test_implementations.py @@ -45,13 +45,13 @@ def test_parameters(self): def test_alert_message(self): self.assertEqual( - self.monitor.alert_message(1), + self.monitor.alert_message([{}]), f"{self.monitor.name} - detected 1 cases. " f"Results available at {self.monitor.result_url()}", ) self.assertEqual( - self.windowed_monitor.alert_message(2), + self.windowed_monitor.alert_message([{}, {}]), f"{self.windowed_monitor.name} - detected 2 cases. " f"Results available at {self.windowed_monitor.result_url()}", )