From b50021e23ad343bff4b2f4176eff6750ee2bafc0 Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Fri, 2 Sep 2022 15:59:49 -0400 Subject: [PATCH 1/6] use dune client --- src/dune_client.py | 296 ----------------------- src/dune_interface.py | 20 -- src/legacy_dune.py | 22 ++ src/query_monitor/base.py | 15 +- src/query_monitor/counter.py | 5 +- src/query_monitor/factory.py | 5 +- src/query_monitor/left_bounded.py | 4 +- src/query_monitor/result_threshold.py | 5 +- src/query_monitor/windowed.py | 4 +- src/runner.py | 5 +- src/slackbot.py | 5 +- tests/e2e/test_dune_client.py | 109 --------- tests/e2e/test_query_runner.py | 3 +- tests/unit/test_dune_response_parsing.py | 145 ----------- tests/unit/test_implementations.py | 4 +- 15 files changed, 48 insertions(+), 599 deletions(-) delete mode 100644 src/dune_client.py delete mode 100644 src/dune_interface.py create mode 100644 src/legacy_dune.py delete mode 100644 tests/e2e/test_dune_client.py delete mode 100644 tests/unit/test_dune_response_parsing.py diff --git a/src/dune_client.py b/src/dune_client.py deleted file mode 100644 index c56c4f8..0000000 --- a/src/dune_client.py +++ /dev/null @@ -1,296 +0,0 @@ -"""" -Basic Dune Client Class responsible for refreshing Dune Queries -Framework built on Dune's API Documentation -https://duneanalytics.notion.site/API-Documentation-1b93d16e0fa941398e15047f643e003a -""" -from __future__ import annotations - -import logging.config -import time -from dataclasses import dataclass -from datetime import datetime -from enum import Enum -from json import JSONDecodeError -from typing import Union, Optional, Any - -from dateutil.parser import parse -import requests -from duneapi.api import DuneAPI -from duneapi.types import DuneRecord -from requests import Response - -from src.dune_interface import DuneInterface -from src.query_monitor.base import QueryBase - -log = logging.getLogger(__name__) -logging.config.fileConfig(fname="logging.conf", disable_existing_loggers=False) - -BASE_URL = "https://api.dune.com/api/v1" - - -class DuneError(Exception): - """Possibilities seen so far - {'error': 'invalid API Key'} - {'error': 'Query not found'} - {'error': 'An internal error occured'} - {'error': 'The requested execution ID (ID: Wonky Job ID) is invalid.'} - """ - - def __init__(self, data: dict[str, str], response_class: str): - super().__init__(f"Can't build {response_class} from {data}") - - -class ExecutionState(Enum): - """ - Enum for possible values of Query Execution - """ - - COMPLETED = "QUERY_STATE_COMPLETED" - EXECUTING = "QUERY_STATE_EXECUTING" - PENDING = "QUERY_STATE_PENDING" - CANCELLED = "QUERY_STATE_CANCELLED" - - -@dataclass -class ExecutionResponse: - """ - Representation of Response from Dune's [Post] Execute Query ID endpoint - """ - - execution_id: str - state: ExecutionState - - @classmethod - def from_dict(cls, data: dict[str, str]) -> ExecutionResponse: - """Constructor from dictionary. See unit test for sample input.""" - return cls( - execution_id=data["execution_id"], state=ExecutionState(data["state"]) - ) - - -@dataclass -class TimeData: - """A collection of all timestamp related values contained within Dune Response""" - - submitted_at: datetime - expires_at: datetime - execution_started_at: datetime - execution_ended_at: Optional[datetime] - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> TimeData: - """Constructor from dictionary. See unit test for sample input.""" - end = data.get("execution_ended_at", None) - return cls( - submitted_at=parse(data["submitted_at"]), - expires_at=parse(data["expires_at"]), - execution_started_at=parse(data["execution_started_at"]), - execution_ended_at=None if end is None else parse(end), - ) - - -@dataclass -class ExecutionStatusResponse: - """ - Representation of Response from Dune's [Get] Execution Status endpoint - """ - - execution_id: str - query_id: int - state: ExecutionState - times: TimeData - - @classmethod - def from_dict(cls, data: dict[str, str]) -> ExecutionStatusResponse: - """Constructor from dictionary. See unit test for sample input.""" - return cls( - execution_id=data["execution_id"], - query_id=int(data["query_id"]), - state=ExecutionState(data["state"]), - times=TimeData.from_dict(data), # Sending the entire data dict - ) - - -@dataclass -class ResultMetadata: - """ - Representation of Dune's Result Metadata from [Get] Query Results endpoint - """ - - column_names: list[str] - result_set_bytes: int - total_row_count: int - - @classmethod - def from_dict(cls, data: dict[str, int | list[str]]) -> ResultMetadata: - """Constructor from dictionary. See unit test for sample input.""" - assert isinstance(data["column_names"], list) - assert isinstance(data["result_set_bytes"], int) - assert isinstance(data["total_row_count"], int) - return cls( - column_names=data["column_names"], - result_set_bytes=int(data["result_set_bytes"]), - total_row_count=int(data["total_row_count"]), - ) - - -RowData = list[dict[str, str]] -MetaData = dict[str, Union[int, list[str]]] - - -@dataclass -class ExecutionResult: - """Representation of `result` field of a Dune ResultsResponse""" - - rows: list[DuneRecord] - metadata: ResultMetadata - - @classmethod - def from_dict(cls, data: dict[str, RowData | MetaData]) -> ExecutionResult: - """Constructor from dictionary. See unit test for sample input.""" - assert isinstance(data["rows"], list) - assert isinstance(data["metadata"], dict) - return cls( - rows=data["rows"], - metadata=ResultMetadata.from_dict(data["metadata"]), - ) - - -ResultData = dict[str, Union[RowData, MetaData]] - - -@dataclass -class ResultsResponse: - """ - Representation of Response from Dune's [Get] Query Results endpoint - """ - - execution_id: str - query_id: int - state: ExecutionState - times: TimeData - result: ExecutionResult - - @classmethod - def from_dict(cls, data: dict[str, str | int | ResultData]) -> ResultsResponse: - """Constructor from dictionary. See unit test for sample input.""" - assert isinstance(data["execution_id"], str) - assert isinstance(data["query_id"], int) - assert isinstance(data["state"], str) - assert isinstance(data["result"], dict) - return cls( - execution_id=data["execution_id"], - query_id=int(data["query_id"]), - state=ExecutionState(data["state"]), - times=TimeData.from_dict(data), - result=ExecutionResult.from_dict(data["result"]), - ) - - -class DuneClient(DuneInterface): - """ - An interface for Dune API with a few convenience methods - combining the use of endpoints (e.g. refresh) - """ - - def __init__(self, api_key: str): - self.token = api_key - - @staticmethod - def _handle_response( - response: Response, - ) -> Any: - try: - # Some responses can be decoded and converted to DuneErrors - response_json = response.json() - log.debug(f"received response {response_json}") - return response_json - except JSONDecodeError as err: - # Others can't. Only raise HTTP error for not decodable errors - response.raise_for_status() - raise ValueError("Unreachable since previous line raises") from err - - def _get(self, url: str) -> Any: - log.debug(f"GET with input url={url}") - response = requests.get(url, headers={"x-dune-api-key": self.token}) - return self._handle_response(response) - - def _post(self, url: str, params: Any) -> Any: - log.debug(f"POST with input url={url}, params={params}") - response = requests.post( - url=url, json=params, headers={"x-dune-api-key": self.token} - ) - return self._handle_response(response) - - def execute(self, query: QueryBase) -> ExecutionResponse: - """Post's to Dune API for execute `query`""" - response_json = self._post( - url=f"{BASE_URL}/query/{query.query_id}/execute", - params={ - "query_parameters": { - # TODO - change query parameters to make class._value_str() public. - param.key: param.to_dict()["value"] - for param in query.parameters() - } - }, - ) - try: - return ExecutionResponse.from_dict(response_json) - except KeyError as err: - raise DuneError(response_json, "ExecutionResponse") from err - - def get_status(self, job_id: str) -> ExecutionStatusResponse: - """GET status from Dune API for `job_id` (aka `execution_id`)""" - response_json = self._get( - url=f"{BASE_URL}/execution/{job_id}/status", - ) - try: - return ExecutionStatusResponse.from_dict(response_json) - except KeyError as err: - raise DuneError(response_json, "ExecutionStatusResponse") from err - - def get_result(self, job_id: str) -> ResultsResponse: - """GET results from Dune API for `job_id` (aka `execution_id`)""" - response_json = self._get(url=f"{BASE_URL}/execution/{job_id}/results") - try: - return ResultsResponse.from_dict(response_json) - except KeyError as err: - raise DuneError(response_json, "ResultsResponse") from err - - def cancel_execution(self, job_id: str) -> bool: - """POST Execution Cancellation to Dune API for `job_id` (aka `execution_id`)""" - response_json = self._post( - url=f"{BASE_URL}/execution/{job_id}/cancel", params=None - ) - try: - # No need to make a dataclass for this since it's just a boolean. - success: bool = response_json["success"] - return success - except KeyError as err: - raise DuneError(response_json, "CancellationResponse") from err - - def refresh(self, query: QueryBase) -> list[DuneRecord]: - """ - Executes a Dune query, waits till query execution completes, - fetches and returns the results. - *Sleeps 3 seconds between each request*. - """ - job_id = self.execute(query).execution_id - while self.get_status(job_id).state != ExecutionState.COMPLETED: - log.info(f"waiting for query execution {job_id} to complete") - # TODO - use a better model for status pings. - time.sleep(5) - - return self.get_result(job_id).result.rows - - -class LegacyDuneClient(DuneInterface): - """Implementation of DuneInterface using the "legacy" (browser emulator) duneapi""" - - def __init__(self, dune: DuneAPI): - self.dune = dune - - def refresh(self, query: QueryBase) -> list[DuneRecord]: - """Executes dune query by ID, and fetches the results by job ID returned""" - job_id = self.dune.execute(query.query_id, query.parameters()) - return self.dune.get_results(job_id) diff --git a/src/dune_interface.py b/src/dune_interface.py deleted file mode 100644 index 1706327..0000000 --- a/src/dune_interface.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Abstract class for a basic Dune Interface with refresh method used by Query Runner. -""" -from abc import ABC - -from duneapi.types import DuneRecord - -from src.query_monitor.base import QueryBase - - -class DuneInterface(ABC): - """ - User Facing Methods for a Dune Client - """ - - def refresh(self, query: QueryBase) -> list[DuneRecord]: - """ - Executes a Dune query, waits till query execution completes, - fetches and returns the results. - """ diff --git a/src/legacy_dune.py b/src/legacy_dune.py new file mode 100644 index 0000000..03c7b28 --- /dev/null +++ b/src/legacy_dune.py @@ -0,0 +1,22 @@ +"""" +Basic Dune Client Class responsible for refreshing Dune Queries +Framework built on Dune's API Documentation +https://duneanalytics.notion.site/API-Documentation-1b93d16e0fa941398e15047f643e003a +""" +from duneapi.api import DuneAPI +from duneapi.types import DuneRecord +from dune_client.interface import DuneInterface +from dune_client.query import Query + + +# TODO - Move This into dune_client. +class LegacyDuneClient(DuneInterface): + """Implementation of DuneInterface using the "legacy" (browser emulator) duneapi""" + + def __init__(self, dune: DuneAPI): + self.dune = dune + + def refresh(self, query: Query) -> list[DuneRecord]: + """Executes dune query by ID, and fetches the results by job ID returned""" + job_id = self.dune.execute(query.query_id, query.parameters()) + return self.dune.get_results(job_id) diff --git a/src/query_monitor/base.py b/src/query_monitor/base.py index cfe2167..ca3f659 100644 --- a/src/query_monitor/base.py +++ b/src/query_monitor/base.py @@ -2,21 +2,12 @@ Abstract class containing Base/Default QueryMonitor attributes. """ from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import Optional from duneapi.types import QueryParameter, DuneRecord - -from src.alert import Alert +from dune_client.query import Query -@dataclass -class QueryData: - """Basic data structure constituting a Dune Analytics Query.""" - - name: str - query_id: int - params: Optional[list[QueryParameter]] = None +from src.alert import Alert class QueryBase(ABC): @@ -25,7 +16,7 @@ class QueryBase(ABC): that are extended on in some implementations. """ - def __init__(self, query: QueryData): + def __init__(self, query: Query): self.query = query @property diff --git a/src/query_monitor/counter.py b/src/query_monitor/counter.py index b0f5e7c..fc2faa8 100644 --- a/src/query_monitor/counter.py +++ b/src/query_monitor/counter.py @@ -1,9 +1,10 @@ """QueryMonitor for Counters. Alert set to valuation""" from duneapi.types import DuneRecord +from dune_client.query import Query from src.alert import Alert, AlertLevel -from src.query_monitor.base import QueryBase, QueryData +from src.query_monitor.base import QueryBase class CounterQueryMonitor(QueryBase): @@ -13,7 +14,7 @@ class CounterQueryMonitor(QueryBase): def __init__( self, - query: QueryData, + query: Query, column: str, alert_value: float = 0.0, ): diff --git a/src/query_monitor/factory.py b/src/query_monitor/factory.py index 4c2928a..34b692b 100644 --- a/src/query_monitor/factory.py +++ b/src/query_monitor/factory.py @@ -5,9 +5,10 @@ import yaml from duneapi.types import QueryParameter +from dune_client.query import Query from src.models import TimeWindow, LeftBound -from src.query_monitor.base import QueryBase, QueryData +from src.query_monitor.base import QueryBase from src.query_monitor.counter import CounterQueryMonitor from src.query_monitor.left_bounded import LeftBoundedQueryMonitor from src.query_monitor.result_threshold import ResultThresholdQuery @@ -19,7 +20,7 @@ def load_from_config(config_yaml: str) -> QueryBase: with open(config_yaml, "r", encoding="utf-8") as yaml_file: cfg = yaml.load(yaml_file, yaml.Loader) - query = QueryData( + query = Query( name=cfg["name"], query_id=cfg["id"], params=[ diff --git a/src/query_monitor/left_bounded.py b/src/query_monitor/left_bounded.py index fd79cc3..a205916 100644 --- a/src/query_monitor/left_bounded.py +++ b/src/query_monitor/left_bounded.py @@ -3,9 +3,9 @@ import urllib.parse from duneapi.types import QueryParameter +from dune_client.query import Query from src.models import LeftBound -from src.query_monitor.base import QueryData from src.query_monitor.result_threshold import ResultThresholdQuery @@ -17,7 +17,7 @@ class LeftBoundedQueryMonitor(ResultThresholdQuery): def __init__( self, - query: QueryData, + query: Query, left_bound: LeftBound, threshold: int = 0, ): diff --git a/src/query_monitor/result_threshold.py b/src/query_monitor/result_threshold.py index 179d0d0..784e436 100644 --- a/src/query_monitor/result_threshold.py +++ b/src/query_monitor/result_threshold.py @@ -3,15 +3,16 @@ number of results returned is > `threshold` """ from duneapi.types import DuneRecord +from dune_client.query import Query from src.alert import Alert, AlertLevel -from src.query_monitor.base import QueryBase, QueryData +from src.query_monitor.base import QueryBase class ResultThresholdQuery(QueryBase): """This is essentially the base query monitor with all default methods""" - def __init__(self, query: QueryData, threshold: int = 0): + def __init__(self, query: Query, threshold: int = 0): super().__init__(query) self.threshold = threshold diff --git a/src/query_monitor/windowed.py b/src/query_monitor/windowed.py index 1f0cdc3..f5a4818 100644 --- a/src/query_monitor/windowed.py +++ b/src/query_monitor/windowed.py @@ -7,9 +7,9 @@ from datetime import datetime, timedelta from duneapi.types import QueryParameter +from dune_client.query import Query from src.models import TimeWindow -from src.query_monitor.base import QueryData from src.query_monitor.result_threshold import ResultThresholdQuery log = logging.getLogger(__name__) @@ -26,7 +26,7 @@ class WindowedQueryMonitor(ResultThresholdQuery): def __init__( self, - query: QueryData, + query: Query, window: TimeWindow, threshold: int = 0, ): diff --git a/src/runner.py b/src/runner.py index 001d4e2..e48b5cc 100644 --- a/src/runner.py +++ b/src/runner.py @@ -7,8 +7,9 @@ import logging.config +from dune_client.interface import DuneInterface + from src.alert import AlertLevel -from src.dune_interface import DuneInterface from src.query_monitor.base import QueryBase from src.slack_client import BasicSlackClient @@ -34,7 +35,7 @@ def run_loop(self) -> None: """ query = self.query log.info(f'Refreshing "{query.name}" query {query.result_url()}') - results = self.dune.refresh(query) + results = self.dune.refresh(query.query) alert = query.get_alert(results) if alert.level == AlertLevel.SLACK: log.warning(alert.message) diff --git a/src/slackbot.py b/src/slackbot.py index 14e6a19..03af0b8 100644 --- a/src/slackbot.py +++ b/src/slackbot.py @@ -6,9 +6,10 @@ import dotenv from duneapi.api import DuneAPI +from dune_client.client import DuneClient +from dune_client.interface import DuneInterface -from src.dune_client import DuneClient, LegacyDuneClient -from src.dune_interface import DuneInterface +from src.legacy_dune import LegacyDuneClient from src.query_monitor.base import QueryBase from src.query_monitor.factory import load_from_config from src.runner import QueryRunner diff --git a/tests/e2e/test_dune_client.py b/tests/e2e/test_dune_client.py deleted file mode 100644 index 7f5fecb..0000000 --- a/tests/e2e/test_dune_client.py +++ /dev/null @@ -1,109 +0,0 @@ -import copy -import os -import time -import unittest - -import dotenv -from requests import JSONDecodeError - -from src.dune_client import ( - DuneClient, - ExecutionResponse, - ExecutionStatusResponse, - ExecutionState, - DuneError, -) -from src.query_monitor.factory import load_from_config - - -class TestDuneClient(unittest.TestCase): - def setUp(self) -> None: - self.query = load_from_config("./tests/data/v2-test-data.yaml") - dotenv.load_dotenv() - self.valid_api_key = os.environ["DUNE_API_KEY"] - - def test_endpoints(self): - dune = DuneClient(self.valid_api_key) - # POST Execution - execution_response = dune.execute(self.query) - self.assertIsInstance(execution_response, ExecutionResponse) - - # GET Execution Status - job_id = execution_response.execution_id - status = dune.get_status(job_id) - self.assertIsInstance(status, ExecutionStatusResponse) - - # GET ExecutionResults - while dune.get_status(job_id).state != ExecutionState.COMPLETED: - time.sleep(1) - results = dune.get_result(job_id).result.rows - self.assertGreater(len(results), 0) - - def test_cancel_execution(self): - dune = DuneClient(self.valid_api_key) - query = load_from_config("./tests/data/v2-long-running-query.yaml") - execution_response = dune.execute(query) - # POST Cancellation - success = dune.cancel_execution(execution_response.execution_id) - self.assertTrue(success) - - def test_invalid_api_key_error(self): - dune = DuneClient(api_key="Invalid Key") - with self.assertRaises(DuneError) as err: - dune.execute(self.query) - self.assertEqual( - str(err.exception), - "Can't build ExecutionResponse from {'error': 'invalid API Key'}", - ) - with self.assertRaises(DuneError) as err: - dune.get_status("wonky job_id") - self.assertEqual( - str(err.exception), - "Can't build ExecutionStatusResponse from {'error': 'invalid API Key'}", - ) - with self.assertRaises(DuneError) as err: - dune.get_result("wonky job_id") - self.assertEqual( - str(err.exception), - "Can't build ResultsResponse from {'error': 'invalid API Key'}", - ) - - def test_query_not_found_error(self): - dune = DuneClient(self.valid_api_key) - query = copy.copy(self.query) - query.query.query_id = 99999999 # Invalid Query Id. - - with self.assertRaises(DuneError) as err: - dune.execute(query) - self.assertEqual( - str(err.exception), - "Can't build ExecutionResponse from {'error': 'Query not found'}", - ) - - def test_internal_error(self): - dune = DuneClient(self.valid_api_key) - query = copy.copy(self.query) - # This query ID is too large! - query.query.query_id = 9999999999999 - - with self.assertRaises(DuneError) as err: - dune.execute(query) - self.assertEqual( - str(err.exception), - "Can't build ExecutionResponse from {'error': 'An internal error occured'}", - ) - - def test_invalid_job_id_error(self): - dune = DuneClient(self.valid_api_key) - - with self.assertRaises(DuneError) as err: - dune.get_status("Wonky Job ID") - self.assertEqual( - str(err.exception), - "Can't build ExecutionStatusResponse from " - "{'error': 'The requested execution ID (ID: Wonky Job ID) is invalid.'}", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/e2e/test_query_runner.py b/tests/e2e/test_query_runner.py index bce09b9..d5f32c8 100644 --- a/tests/e2e/test_query_runner.py +++ b/tests/e2e/test_query_runner.py @@ -3,8 +3,9 @@ from unittest.mock import patch import dotenv +from dune_client.client import DuneClient -from src.dune_client import DuneClient +from src.legacy_dune import LegacyDuneClient from src.query_monitor.factory import load_from_config from src.runner import QueryRunner from src.slack_client import BasicSlackClient diff --git a/tests/unit/test_dune_response_parsing.py b/tests/unit/test_dune_response_parsing.py deleted file mode 100644 index 4529ade..0000000 --- a/tests/unit/test_dune_response_parsing.py +++ /dev/null @@ -1,145 +0,0 @@ -import unittest -from datetime import datetime - -from dateutil.tz import tzutc - -from src.dune_client import ( - ExecutionResponse, - ExecutionStatusResponse, - ExecutionState, - ResultsResponse, - TimeData, - ExecutionResult, - ResultMetadata, -) - - -class MyTestCase(unittest.TestCase): - def setUp(self) -> None: - self.execution_id = "01GBM4W2N0NMCGPZYW8AYK4YF1" - self.query_id = 980708 - - self.execution_response_data = { - "execution_id": self.execution_id, - "state": "QUERY_STATE_PENDING", - } - self.status_response_data = { - "execution_id": self.execution_id, - "query_id": self.query_id, - "state": "QUERY_STATE_EXECUTING", - "submitted_at": "2022-08-29T06:33:24.913138Z", - "expires_at": "1970-01-01T00:00:00Z", - "execution_started_at": "2022-08-29T06:33:24.916543331Z", - } - self.results_response_data = { - "execution_id": self.execution_id, - "query_id": self.query_id, - "state": "QUERY_STATE_COMPLETED", - "submitted_at": "2022-08-29T06:33:24.913138Z", - "expires_at": "2024-08-28T06:36:41.58847Z", - "execution_started_at": "2022-08-29T06:33:24.916543Z", - "execution_ended_at": "2022-08-29T06:36:41.588467Z", - "result": { - "rows": [ - {"TableName": "eth_blocks", "ct": 6296}, - {"TableName": "eth_traces", "ct": 4474223}, - ], - "metadata": { - "column_names": ["ct", "TableName"], - "result_set_bytes": 194, - "total_row_count": 8, - }, - }, - } - - def test_execution_response_parsing(self): - expected = ExecutionResponse( - execution_id="01GBM4W2N0NMCGPZYW8AYK4YF1", - state=ExecutionState.PENDING, - ) - - self.assertEqual( - expected, ExecutionResponse.from_dict(self.execution_response_data) - ) - - def test_parse_time_data(self): - expected_with_end = TimeData( - submitted_at=datetime(2022, 8, 29, 6, 33, 24, 913138, tzinfo=tzutc()), - expires_at=datetime(2024, 8, 28, 6, 36, 41, 588470, tzinfo=tzutc()), - execution_started_at=datetime( - 2022, 8, 29, 6, 33, 24, 916543, tzinfo=tzutc() - ), - execution_ended_at=datetime(2022, 8, 29, 6, 36, 41, 588467, tzinfo=tzutc()), - ) - self.assertEqual( - expected_with_end, TimeData.from_dict(self.results_response_data) - ) - - expected_without_end = TimeData( - submitted_at=datetime(2022, 8, 29, 6, 33, 24, 913138, tzinfo=tzutc()), - expires_at=datetime(1970, 1, 1, 0, 0, tzinfo=tzutc()), - execution_started_at=datetime( - 2022, 8, 29, 6, 33, 24, 916543, tzinfo=tzutc() - ), - execution_ended_at=None, - ) - self.assertEqual( - expected_without_end, TimeData.from_dict(self.status_response_data) - ) - - def test_parse_status_response(self): - expected = ExecutionStatusResponse( - execution_id="01GBM4W2N0NMCGPZYW8AYK4YF1", - query_id=980708, - state=ExecutionState.EXECUTING, - times=TimeData.from_dict(self.status_response_data), - ) - self.assertEqual( - expected, ExecutionStatusResponse.from_dict(self.status_response_data) - ) - - def test_parse_result_metadata(self): - expected = ResultMetadata( - column_names=["ct", "TableName"], - result_set_bytes=194, - total_row_count=8, - ) - self.assertEqual( - expected, - ResultMetadata.from_dict(self.results_response_data["result"]["metadata"]), - ) - - def test_parse_execution_result(self): - expected = ExecutionResult( - rows=[ - {"TableName": "eth_blocks", "ct": 6296}, - {"TableName": "eth_traces", "ct": 4474223}, - ], - # Parsing tested above in test_result_metadata_parsing - metadata=ResultMetadata.from_dict( - self.results_response_data["result"]["metadata"] - ), - ) - - self.assertEqual( - expected, ExecutionResult.from_dict(self.results_response_data["result"]) - ) - - def test_parse_result_response(self): - # Time data parsing tested above in test_time_data_parsing. - time_data = TimeData.from_dict(self.results_response_data) - expected = ResultsResponse( - execution_id=self.execution_id, - query_id=self.query_id, - state=ExecutionState.COMPLETED, - times=time_data, - # Execution result parsing tested above in test_execution_result - result=ExecutionResult.from_dict(self.results_response_data["result"]), - ) - self.assertEqual( - expected, ResultsResponse.from_dict(self.results_response_data) - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/unit/test_implementations.py b/tests/unit/test_implementations.py index 77a8388..635a164 100644 --- a/tests/unit/test_implementations.py +++ b/tests/unit/test_implementations.py @@ -1,10 +1,10 @@ import datetime import unittest +from dune_client.query import Query from duneapi.types import QueryParameter from src.alert import Alert, AlertLevel -from src.query_monitor.base import QueryData from src.query_monitor.counter import CounterQueryMonitor from src.query_monitor.factory import load_from_config from src.query_monitor.result_threshold import ResultThresholdQuery @@ -21,7 +21,7 @@ def setUp(self) -> None: QueryParameter.number_type("Text", 12), QueryParameter.date_type("Date", "2021-01-01 12:34:56"), ] - query = QueryData(name="Monitor", query_id=0, params=self.query_params) + query = Query(name="Monitor", query_id=0, params=self.query_params) self.monitor = ResultThresholdQuery(query) self.windowed_monitor = WindowedQueryMonitor( query, From f44f97e7c0862422694a2a555cb92f987346d7bc Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Mon, 5 Sep 2022 08:06:19 -0400 Subject: [PATCH 2/6] add dune client dependency --- requirements/prod.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/prod.txt b/requirements/prod.txt index 29fa9bc..e676c1e 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,4 +1,5 @@ duneapi==4.0.0 +dune-client==0.0.0 slackclient==2.9.4 PyYAML==6.0 types-python-dateutil==2.8.19 From 1800d9da64bfd8c22d47e517078ab7ec77903bea Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Mon, 5 Sep 2022 09:32:40 -0400 Subject: [PATCH 3/6] remove legacy dune client --- requirements/dev.txt | 6 +++--- requirements/prod.txt | 7 +++---- src/legacy_dune.py | 22 ---------------------- src/models.py | 3 +-- src/query_monitor/base.py | 2 +- src/query_monitor/counter.py | 2 +- src/query_monitor/factory.py | 2 +- src/query_monitor/left_bounded.py | 2 +- src/query_monitor/result_threshold.py | 2 +- src/query_monitor/windowed.py | 2 +- src/slackbot.py | 14 +------------- tests/e2e/test_query_runner.py | 1 - tests/unit/test_implementations.py | 4 ++-- tests/unit/test_models.py | 3 +-- 14 files changed, 17 insertions(+), 55 deletions(-) delete mode 100644 src/legacy_dune.py diff --git a/requirements/dev.txt b/requirements/dev.txt index 26b58e5..681d75f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r prod.txt -black==22.6.0 -pylint==2.14.5 -pytest==7.1.2 +black==22.8.0 +pylint==2.15.0 +pytest==7.1.3 mypy==0.971 \ No newline at end of file diff --git a/requirements/prod.txt b/requirements/prod.txt index e676c1e..deb1def 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,9 +1,8 @@ -duneapi==4.0.0 -dune-client==0.0.0 +dune-client==0.0.1 slackclient==2.9.4 PyYAML==6.0 types-python-dateutil==2.8.19 -types-PyYAML==6.0.10 +types-PyYAML==6.0.11 python-dateutil==2.8.2 -python-dotenv==0.20.0 +python-dotenv==0.21.0 certifi==2022.6.15 \ No newline at end of file diff --git a/src/legacy_dune.py b/src/legacy_dune.py deleted file mode 100644 index 03c7b28..0000000 --- a/src/legacy_dune.py +++ /dev/null @@ -1,22 +0,0 @@ -"""" -Basic Dune Client Class responsible for refreshing Dune Queries -Framework built on Dune's API Documentation -https://duneanalytics.notion.site/API-Documentation-1b93d16e0fa941398e15047f643e003a -""" -from duneapi.api import DuneAPI -from duneapi.types import DuneRecord -from dune_client.interface import DuneInterface -from dune_client.query import Query - - -# TODO - Move This into dune_client. -class LegacyDuneClient(DuneInterface): - """Implementation of DuneInterface using the "legacy" (browser emulator) duneapi""" - - def __init__(self, dune: DuneAPI): - self.dune = dune - - def refresh(self, query: Query) -> list[DuneRecord]: - """Executes dune query by ID, and fetches the results by job ID returned""" - job_id = self.dune.execute(query.query_id, query.parameters()) - return self.dune.get_results(job_id) diff --git a/src/models.py b/src/models.py index b63980e..3ac67d2 100644 --- a/src/models.py +++ b/src/models.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta, date from enum import Enum -from duneapi.types import QueryParameter +from dune_client.types import QueryParameter class TimeWindow: @@ -87,7 +87,6 @@ def as_query_parameters(self) -> list[QueryParameter]: QueryParameter.enum_type( name="TimeUnits", value=str(self.units.value), - options=TimeUnit.options(), ), QueryParameter.number_type(name="Offset", value=self.offset), ] diff --git a/src/query_monitor/base.py b/src/query_monitor/base.py index ca3f659..bd624d1 100644 --- a/src/query_monitor/base.py +++ b/src/query_monitor/base.py @@ -3,7 +3,7 @@ """ from abc import ABC, abstractmethod -from duneapi.types import QueryParameter, DuneRecord +from dune_client.types import QueryParameter, DuneRecord from dune_client.query import Query diff --git a/src/query_monitor/counter.py b/src/query_monitor/counter.py index fc2faa8..41e098c 100644 --- a/src/query_monitor/counter.py +++ b/src/query_monitor/counter.py @@ -1,6 +1,6 @@ """QueryMonitor for Counters. Alert set to valuation""" -from duneapi.types import DuneRecord +from dune_client.types import DuneRecord from dune_client.query import Query from src.alert import Alert, AlertLevel diff --git a/src/query_monitor/factory.py b/src/query_monitor/factory.py index 34b692b..2ab7f82 100644 --- a/src/query_monitor/factory.py +++ b/src/query_monitor/factory.py @@ -4,7 +4,7 @@ from __future__ import annotations import yaml -from duneapi.types import QueryParameter +from dune_client.types import QueryParameter from dune_client.query import Query from src.models import TimeWindow, LeftBound diff --git a/src/query_monitor/left_bounded.py b/src/query_monitor/left_bounded.py index a205916..7fd5637 100644 --- a/src/query_monitor/left_bounded.py +++ b/src/query_monitor/left_bounded.py @@ -2,7 +2,7 @@ from __future__ import annotations import urllib.parse -from duneapi.types import QueryParameter +from dune_client.types import QueryParameter from dune_client.query import Query from src.models import LeftBound diff --git a/src/query_monitor/result_threshold.py b/src/query_monitor/result_threshold.py index 784e436..14e0a8d 100644 --- a/src/query_monitor/result_threshold.py +++ b/src/query_monitor/result_threshold.py @@ -2,7 +2,7 @@ Elementary implementation of QueryBase that alerts when number of results returned is > `threshold` """ -from duneapi.types import DuneRecord +from dune_client.types import DuneRecord from dune_client.query import Query from src.alert import Alert, AlertLevel diff --git a/src/query_monitor/windowed.py b/src/query_monitor/windowed.py index f5a4818..b4eff94 100644 --- a/src/query_monitor/windowed.py +++ b/src/query_monitor/windowed.py @@ -6,7 +6,7 @@ import urllib.parse from datetime import datetime, timedelta -from duneapi.types import QueryParameter +from dune_client.types import QueryParameter from dune_client.query import Query from src.models import TimeWindow diff --git a/src/slackbot.py b/src/slackbot.py index 03af0b8..18a5e03 100644 --- a/src/slackbot.py +++ b/src/slackbot.py @@ -5,11 +5,9 @@ import os import dotenv -from duneapi.api import DuneAPI from dune_client.client import DuneClient from dune_client.interface import DuneInterface -from src.legacy_dune import LegacyDuneClient from src.query_monitor.base import QueryBase from src.query_monitor.factory import load_from_config from src.runner import QueryRunner @@ -35,21 +33,11 @@ def run_slackbot( help="YAML configuration file for a QueryMonitor object", required=True, ) - parser.add_argument( - "--use-legacy-dune", - type=bool, - help="Indicate whether legacy duneapi client should be used.", - default=False, - ) args = parser.parse_args() dotenv.load_dotenv() run_slackbot( query=load_from_config(args.query_config), - dune=( - LegacyDuneClient(DuneAPI.new_from_environment()) - if args.use_legacy_dune - else DuneClient(os.environ["DUNE_API_KEY"]) - ), + dune=DuneClient(os.environ["DUNE_API_KEY"]), slack_client=BasicSlackClient( token=os.environ["SLACK_TOKEN"], channel=os.environ["SLACK_ALERT_CHANNEL"] ), diff --git a/tests/e2e/test_query_runner.py b/tests/e2e/test_query_runner.py index d5f32c8..be96a20 100644 --- a/tests/e2e/test_query_runner.py +++ b/tests/e2e/test_query_runner.py @@ -5,7 +5,6 @@ import dotenv from dune_client.client import DuneClient -from src.legacy_dune import LegacyDuneClient from src.query_monitor.factory import load_from_config from src.runner import QueryRunner from src.slack_client import BasicSlackClient diff --git a/tests/unit/test_implementations.py b/tests/unit/test_implementations.py index 635a164..406902e 100644 --- a/tests/unit/test_implementations.py +++ b/tests/unit/test_implementations.py @@ -2,7 +2,7 @@ import unittest from dune_client.query import Query -from duneapi.types import QueryParameter +from dune_client.types import QueryParameter from src.alert import Alert, AlertLevel from src.query_monitor.counter import CounterQueryMonitor @@ -16,7 +16,7 @@ class TestQueryMonitor(unittest.TestCase): def setUp(self) -> None: self.date = datetime.datetime(year=1985, month=3, day=10) self.query_params = [ - QueryParameter.enum_type("Enum", "option1", ["option1", "option2"]), + QueryParameter.enum_type("Enum", "option1"), QueryParameter.text_type("Text", "option1"), QueryParameter.number_type("Text", 12), QueryParameter.date_type("Date", "2021-01-01 12:34:56"), diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 9302984..effd984 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -2,7 +2,7 @@ from datetime import timedelta, datetime -from duneapi.types import QueryParameter +from dune_client.types import QueryParameter from src.models import LeftBound, TimeUnit, TimeWindow @@ -101,7 +101,6 @@ def test_as_query_params(self): QueryParameter.enum_type( name="TimeUnits", value=str(TimeUnit.MINUTES.value), - options=TimeUnit.options(), ), QueryParameter.number_type(name="Offset", value=1), ], From 20ce2c25a45737fe995bb54a6f3d47e784f94ffa Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Mon, 5 Sep 2022 12:30:17 -0400 Subject: [PATCH 4/6] update query parameters for specialized monitor classes --- src/legacy_dune.py | 22 ---------------------- src/query_monitor/base.py | 9 +-------- src/query_monitor/left_bounded.py | 6 +----- src/query_monitor/windowed.py | 7 ++----- tests/unit/test_implementations.py | 15 ++++++++------- 5 files changed, 12 insertions(+), 47 deletions(-) delete mode 100644 src/legacy_dune.py diff --git a/src/legacy_dune.py b/src/legacy_dune.py deleted file mode 100644 index 03c7b28..0000000 --- a/src/legacy_dune.py +++ /dev/null @@ -1,22 +0,0 @@ -"""" -Basic Dune Client Class responsible for refreshing Dune Queries -Framework built on Dune's API Documentation -https://duneanalytics.notion.site/API-Documentation-1b93d16e0fa941398e15047f643e003a -""" -from duneapi.api import DuneAPI -from duneapi.types import DuneRecord -from dune_client.interface import DuneInterface -from dune_client.query import Query - - -# TODO - Move This into dune_client. -class LegacyDuneClient(DuneInterface): - """Implementation of DuneInterface using the "legacy" (browser emulator) duneapi""" - - def __init__(self, dune: DuneAPI): - self.dune = dune - - def refresh(self, query: Query) -> list[DuneRecord]: - """Executes dune query by ID, and fetches the results by job ID returned""" - job_id = self.dune.execute(query.query_id, query.parameters()) - return self.dune.get_results(job_id) diff --git a/src/query_monitor/base.py b/src/query_monitor/base.py index bd624d1..8f18bee 100644 --- a/src/query_monitor/base.py +++ b/src/query_monitor/base.py @@ -3,7 +3,7 @@ """ from abc import ABC, abstractmethod -from dune_client.types import QueryParameter, DuneRecord +from dune_client.types import DuneRecord from dune_client.query import Query @@ -29,13 +29,6 @@ def name(self) -> str: """Returns (nested) query name - for easier access""" return self.query.name - def parameters(self) -> list[QueryParameter]: - """ - Base implementation only has fixed parameters, - extensions (like WindowedQueryMonitor) would append additional parameters to the fixed ones - """ - return self.query.params or [] - def result_url(self) -> str: """Returns a link to query results excluding fixed parameters""" return f"https://dune.com/queries/{self.query_id}" diff --git a/src/query_monitor/left_bounded.py b/src/query_monitor/left_bounded.py index 7fd5637..98133c2 100644 --- a/src/query_monitor/left_bounded.py +++ b/src/query_monitor/left_bounded.py @@ -2,7 +2,6 @@ from __future__ import annotations import urllib.parse -from dune_client.types import QueryParameter from dune_client.query import Query from src.models import LeftBound @@ -23,10 +22,7 @@ def __init__( ): super().__init__(query, threshold) self.left_bound = left_bound - - def parameters(self) -> list[QueryParameter]: - """Similar to the base model, but with left bound parameter appended""" - return (self.query.params or []) + self.left_bound.as_query_parameters() + self.query.params = self.query.parameters() + left_bound.as_query_parameters() def result_url(self) -> str: """Returns a link to the query""" diff --git a/src/query_monitor/windowed.py b/src/query_monitor/windowed.py index b4eff94..143feac 100644 --- a/src/query_monitor/windowed.py +++ b/src/query_monitor/windowed.py @@ -6,7 +6,6 @@ import urllib.parse from datetime import datetime, timedelta -from dune_client.types import QueryParameter from dune_client.query import Query from src.models import TimeWindow @@ -32,10 +31,8 @@ def __init__( ): super().__init__(query, threshold) self._set_window(window) - - def parameters(self) -> list[QueryParameter]: - """Similar to the base model, but with window parameters appended""" - return (self.query.params or []) + self.window.as_query_parameters() + # Need to update the Query Parameters + self.query.params = self.query.parameters() + self.window.as_query_parameters() def result_url(self) -> str: """Returns a link to the query""" diff --git a/tests/unit/test_implementations.py b/tests/unit/test_implementations.py index 406902e..d4a705a 100644 --- a/tests/unit/test_implementations.py +++ b/tests/unit/test_implementations.py @@ -17,18 +17,19 @@ def setUp(self) -> None: self.date = datetime.datetime(year=1985, month=3, day=10) self.query_params = [ QueryParameter.enum_type("Enum", "option1"), - QueryParameter.text_type("Text", "option1"), - QueryParameter.number_type("Text", 12), + QueryParameter.text_type("Text", "plain text"), + QueryParameter.number_type("Number", 12), QueryParameter.date_type("Date", "2021-01-01 12:34:56"), ] - query = Query(name="Monitor", query_id=0, params=self.query_params) - self.monitor = ResultThresholdQuery(query) + self.monitor = ResultThresholdQuery( + query=Query(name="Monitor", query_id=0, params=self.query_params) + ) self.windowed_monitor = WindowedQueryMonitor( - query, + query=Query(name="Windowed Monitor", query_id=0, params=self.query_params), window=TimeWindow(start=self.date), ) self.counter = CounterQueryMonitor( - query, + query=Query(name="Counter Monitor", query_id=0, params=self.query_params), column="col_name", alert_value=1.0, ) @@ -71,7 +72,7 @@ def test_alert_message(self): ctr.get_alert([{ctr.column: ctr.alert_value + 1}]), Alert( level=AlertLevel.SLACK, - message=f"Query Monitor: {ctr.column} exceeds {ctr.alert_value} " + message=f"Query Counter Monitor: {ctr.column} exceeds {ctr.alert_value} " f"with {ctr.alert_value + 1} (cf. https://dune.com/queries/{ctr.query_id})", ), ) From 85522266e55ddab8e01c5744efc713e66eeeafce Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Mon, 5 Sep 2022 12:46:25 -0400 Subject: [PATCH 5/6] fall back on query parameters and URL --- requirements/prod.txt | 2 +- src/alert.py | 12 +++++++++++- src/query_monitor/base.py | 10 ++++++++-- src/query_monitor/counter.py | 8 +++----- src/query_monitor/left_bounded.py | 10 ---------- src/query_monitor/result_threshold.py | 2 +- src/query_monitor/windowed.py | 13 ++----------- tests/unit/test_implementations.py | 9 +-------- 8 files changed, 27 insertions(+), 39 deletions(-) diff --git a/requirements/prod.txt b/requirements/prod.txt index deb1def..fa5e7b4 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,4 +1,4 @@ -dune-client==0.0.1 +dune-client==0.0.2 slackclient==2.9.4 PyYAML==6.0 types-python-dateutil==2.8.19 diff --git a/src/alert.py b/src/alert.py index 2934edb..81c65d1 100644 --- a/src/alert.py +++ b/src/alert.py @@ -28,4 +28,14 @@ class Alert: @classmethod def default(cls) -> Alert: """Default alert level is non with no message.""" - return Alert(AlertLevel.NONE, "") + return cls(AlertLevel.NONE, "") + + @classmethod + def log(cls, message: str) -> Alert: + """Easy Log Constructor""" + return cls(AlertLevel.LOG, message) + + @classmethod + def slack(cls, message: str) -> Alert: + """Easy Slack variant constructor""" + return cls(AlertLevel.SLACK, message) diff --git a/src/query_monitor/base.py b/src/query_monitor/base.py index 8f18bee..fab9704 100644 --- a/src/query_monitor/base.py +++ b/src/query_monitor/base.py @@ -3,7 +3,7 @@ """ from abc import ABC, abstractmethod -from dune_client.types import DuneRecord +from dune_client.types import DuneRecord, QueryParameter from dune_client.query import Query @@ -29,9 +29,15 @@ def name(self) -> str: """Returns (nested) query name - for easier access""" return self.query.name + def parameters(self) -> list[QueryParameter]: + """ + Returning the inner query's parameters. + """ + return self.query.parameters() + def result_url(self) -> str: """Returns a link to query results excluding fixed parameters""" - return f"https://dune.com/queries/{self.query_id}" + return self.query.url() @abstractmethod def get_alert(self, results: list[DuneRecord]) -> Alert: diff --git a/src/query_monitor/counter.py b/src/query_monitor/counter.py index 41e098c..764c6de 100644 --- a/src/query_monitor/counter.py +++ b/src/query_monitor/counter.py @@ -3,7 +3,7 @@ from dune_client.types import DuneRecord from dune_client.query import Query -from src.alert import Alert, AlertLevel +from src.alert import Alert from src.query_monitor.base import QueryBase @@ -29,13 +29,11 @@ def _result_value(self, results: list[DuneRecord]) -> float: def get_alert(self, results: list[DuneRecord]) -> Alert: result_value = self._result_value(results) if result_value > self.alert_value: - return Alert( - level=AlertLevel.SLACK, + return Alert.slack( message=f"Query {self.name}: {self.column} exceeds {self.alert_value} " f"with {self._result_value(results)} (cf. {self.result_url()})", ) - return Alert( - level=AlertLevel.LOG, + return Alert.log( message=f"value of {self.column} = {result_value} " f"does not exceed {self.alert_value}", ) diff --git a/src/query_monitor/left_bounded.py b/src/query_monitor/left_bounded.py index 98133c2..e1d5b2b 100644 --- a/src/query_monitor/left_bounded.py +++ b/src/query_monitor/left_bounded.py @@ -1,6 +1,5 @@ """Implementation of BaseQueryMonitor for queries beginning from StartTime""" from __future__ import annotations -import urllib.parse from dune_client.query import Query @@ -23,12 +22,3 @@ def __init__( super().__init__(query, threshold) self.left_bound = left_bound self.query.params = self.query.parameters() + left_bound.as_query_parameters() - - def result_url(self) -> str: - """Returns a link to the query""" - base = super().result_url() - # Include variable parameters in the URL so they are set - query = "&".join( - [f"{p.key}={p.value}" for p in self.left_bound.as_query_parameters()] - ) - return "?".join([base, urllib.parse.quote_plus(query, safe="=&?")]) diff --git a/src/query_monitor/result_threshold.py b/src/query_monitor/result_threshold.py index 14e0a8d..f5342d8 100644 --- a/src/query_monitor/result_threshold.py +++ b/src/query_monitor/result_threshold.py @@ -28,4 +28,4 @@ def get_alert(self, results: list[DuneRecord]) -> Alert: message=f"{self.name} - detected {num_results} cases. " f"Results available at {self.result_url()}", ) - return Alert.default() + return Alert.log("No alert-worthy results detected.") diff --git a/src/query_monitor/windowed.py b/src/query_monitor/windowed.py index 143feac..334b10b 100644 --- a/src/query_monitor/windowed.py +++ b/src/query_monitor/windowed.py @@ -3,7 +3,6 @@ """ from __future__ import annotations import logging.config -import urllib.parse from datetime import datetime, timedelta from dune_client.query import Query @@ -34,18 +33,10 @@ def __init__( # Need to update the Query Parameters self.query.params = self.query.parameters() + self.window.as_query_parameters() - def result_url(self) -> str: - """Returns a link to the query""" - base = super().result_url() - # Include variable parameters in the URL so they are set - query = "&".join( - [f"{p.key}={p.value}" for p in self.window.as_query_parameters()] - ) - return "?".join([base, urllib.parse.quote_plus(query, safe="=&?")]) - def _set_window(self, window: TimeWindow) -> None: if window.end > datetime.now() - timedelta(hours=2): log.warning( - "window end time is beyond 2 hours in the past, some data may not yet be available" + "window end time is beyond 2 hours in the past, " + "some data may not yet be available" ) self.window = window diff --git a/tests/unit/test_implementations.py b/tests/unit/test_implementations.py index d4a705a..b8470a9 100644 --- a/tests/unit/test_implementations.py +++ b/tests/unit/test_implementations.py @@ -34,13 +34,6 @@ def setUp(self) -> None: alert_value=1.0, ) - def test_result_url(self): - self.assertEqual(self.monitor.result_url(), "https://dune.com/queries/0") - self.assertEqual( - self.windowed_monitor.result_url(), - "https://dune.com/queries/0?StartTime=1985-03-10+00%3A00%3A00&EndTime=1985-03-10+06%3A00%3A00", - ) - def test_parameters(self): self.assertEqual(self.monitor.parameters(), self.query_params) self.assertEqual( @@ -73,7 +66,7 @@ def test_alert_message(self): Alert( level=AlertLevel.SLACK, message=f"Query Counter Monitor: {ctr.column} exceeds {ctr.alert_value} " - f"with {ctr.alert_value + 1} (cf. https://dune.com/queries/{ctr.query_id})", + f"with {ctr.alert_value + 1} (cf. {ctr.result_url()})", ), ) From 27281a25d947b42013bcd85cc1b941adce15fd8e Mon Sep 17 00:00:00 2001 From: Ben Smith Date: Mon, 5 Sep 2022 13:15:27 -0400 Subject: [PATCH 6/6] bump to stable version again --- requirements/prod.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/prod.txt b/requirements/prod.txt index fa5e7b4..f67797a 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,4 +1,4 @@ -dune-client==0.0.2 +dune-client==0.0.3 slackclient==2.9.4 PyYAML==6.0 types-python-dateutil==2.8.19