Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preemptive Refactor #15

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[MASTER]
disable=fixme,logging-fstring-interpolation,too-many-arguments
disable=fixme,logging-fstring-interpolation,too-many-arguments,too-few-public-methods
47 changes: 28 additions & 19 deletions src/query_monitor/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]:
"""
Expand All @@ -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):
Expand Down
34 changes: 34 additions & 0 deletions src/slack_client.py
Original file line number Diff line number Diff line change
@@ -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,
)
18 changes: 6 additions & 12 deletions src/slackbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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()
4 changes: 2 additions & 2 deletions tests/test_implementations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()}",
)
Expand Down