strideutils 0.8.4__tar.gz → 0.8.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {strideutils-0.8.4 → strideutils-0.8.6}/.bumpversion.cfg +1 -1
  2. {strideutils-0.8.4 → strideutils-0.8.6}/PKG-INFO +1 -1
  3. {strideutils-0.8.4 → strideutils-0.8.6}/pyproject.toml +1 -1
  4. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/exec_tx.py +3 -3
  5. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/exec_tx.pyi +1 -1
  6. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/sheets_connector.py +35 -2
  7. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/sheets_connector.pyi +2 -0
  8. strideutils-0.8.6/strideutils/slack_connector.py +183 -0
  9. strideutils-0.8.6/strideutils/slack_connector.pyi +29 -0
  10. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/stride_config.py +61 -78
  11. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/stride_config.pyi +4 -2
  12. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/exec_tx.pyi +1 -1
  13. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/sheets_connector.pyi +3 -0
  14. strideutils-0.8.6/strideutils-stubs/slack_connector.pyi +37 -0
  15. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/stride_config.pyi +4 -2
  16. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils.egg-info/PKG-INFO +1 -1
  17. {strideutils-0.8.4 → strideutils-0.8.6}/tests/test_config.py +4 -4
  18. strideutils-0.8.4/strideutils/slack_connector.py +0 -147
  19. strideutils-0.8.4/strideutils/slack_connector.pyi +0 -13
  20. strideutils-0.8.4/strideutils-stubs/slack_connector.pyi +0 -24
  21. {strideutils-0.8.4 → strideutils-0.8.6}/.github/workflows/lint.yml +0 -0
  22. {strideutils-0.8.4 → strideutils-0.8.6}/.github/workflows/publish.yml +0 -0
  23. {strideutils-0.8.4 → strideutils-0.8.6}/.github/workflows/test.yml +0 -0
  24. {strideutils-0.8.4 → strideutils-0.8.6}/.github/workflows/version.yaml +0 -0
  25. {strideutils-0.8.4 → strideutils-0.8.6}/.gitignore +0 -0
  26. {strideutils-0.8.4 → strideutils-0.8.6}/LICENSE +0 -0
  27. {strideutils-0.8.4 → strideutils-0.8.6}/MANIFEST.in +0 -0
  28. {strideutils-0.8.4 → strideutils-0.8.6}/Makefile +0 -0
  29. {strideutils-0.8.4 → strideutils-0.8.6}/README.md +0 -0
  30. {strideutils-0.8.4 → strideutils-0.8.6}/example-config.yaml +0 -0
  31. {strideutils-0.8.4 → strideutils-0.8.6}/setup.cfg +0 -0
  32. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/Makefile +0 -0
  33. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/__init__.py +0 -0
  34. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/__init__.pyi +0 -0
  35. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/coingecko.py +0 -0
  36. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/coingecko.pyi +0 -0
  37. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/config_examples/env.local.example +0 -0
  38. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/config_examples/strideutils_config.yaml.example +0 -0
  39. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/cryptography.py +0 -0
  40. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/cryptography.pyi +0 -0
  41. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/py.typed +0 -0
  42. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/redis_connector.py +0 -0
  43. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/redis_connector.pyi +0 -0
  44. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/run_safely.py +0 -0
  45. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/run_safely.pyi +0 -0
  46. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/stride_alerts.py +0 -0
  47. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/stride_alerts.pyi +0 -0
  48. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/stride_requests.py +0 -0
  49. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/stride_requests.pyi +0 -0
  50. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/twilio_connector.py +0 -0
  51. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/twilio_connector.pyi +0 -0
  52. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/types.py +0 -0
  53. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils/types.pyi +0 -0
  54. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/__init__.pyi +0 -0
  55. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/coingecko.pyi +0 -0
  56. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/cryptography.pyi +0 -0
  57. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/redis_connector.pyi +0 -0
  58. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/run_safely.pyi +0 -0
  59. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/stride_alerts.pyi +0 -0
  60. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/stride_requests.pyi +0 -0
  61. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/twilio_connector.pyi +0 -0
  62. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils-stubs/types.pyi +0 -0
  63. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils.egg-info/SOURCES.txt +0 -0
  64. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils.egg-info/dependency_links.txt +0 -0
  65. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils.egg-info/requires.txt +0 -0
  66. {strideutils-0.8.4 → strideutils-0.8.6}/strideutils.egg-info/top_level.txt +0 -0
  67. {strideutils-0.8.4 → strideutils-0.8.6}/tests/test_coingecko.py +0 -0
  68. {strideutils-0.8.4 → strideutils-0.8.6}/tests/test_encryption.py +0 -0
  69. {strideutils-0.8.4 → strideutils-0.8.6}/tests/test_redis_connector.py +0 -0
  70. {strideutils-0.8.4 → strideutils-0.8.6}/tests/test_sheets_connector.py +0 -0
  71. {strideutils-0.8.4 → strideutils-0.8.6}/tests/test_slack_connector.py +0 -0
  72. {strideutils-0.8.4 → strideutils-0.8.6}/tests/test_stride_requests.py +0 -0
  73. {strideutils-0.8.4 → strideutils-0.8.6}/tests/test_twilio_connector.py +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.8.4
2
+ current_version = 0.8.6
3
3
  commit = False
4
4
  tag = False
5
5
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: strideutils
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: Utility functions for monitoring the Stride blockchain
5
5
  Author-email: Stride Labs <hello@stridelabs.co>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "strideutils"
7
- version = "0.8.4"
7
+ version = "0.8.6"
8
8
  authors = [
9
9
  {name = "Stride Labs", email = "hello@stridelabs.co"},
10
10
  ]
@@ -8,7 +8,7 @@ from typing import Optional
8
8
  from strideutils import stride_requests
9
9
  from strideutils.stride_config import config
10
10
 
11
- WALLET_SETUP_COMMAND = "echo {mnemonic} | {binary} keys add {wallet_name} --recover --keyring-backend=test"
11
+ WALLET_SETUP_COMMAND = "echo {mnemonic} | {binary} keys add {account_name} --recover --keyring-backend=test"
12
12
 
13
13
 
14
14
  @dataclass
@@ -22,7 +22,7 @@ class TxResponse:
22
22
  raw_log: Optional[str] = None
23
23
 
24
24
 
25
- def restore_wallet(wallet_name: str = "wallet", mnemonic_var: str = "MNEMONIC", binary: str = "strided"):
25
+ def restore_wallet(account_name: str = "wallet", mnemonic_var: str = "MNEMONIC", binary: str = "strided"):
26
26
  """
27
27
  Restores a wallet in the keyring, using the mnemonic under the specified environment variable
28
28
  """
@@ -30,7 +30,7 @@ def restore_wallet(wallet_name: str = "wallet", mnemonic_var: str = "MNEMONIC",
30
30
  if not mnemonic:
31
31
  raise EnvironmentError(f"{mnemonic_var} must be set")
32
32
 
33
- os.system(WALLET_SETUP_COMMAND.format(mnemonic=mnemonic, binary=binary, wallet_name=wallet_name))
33
+ os.system(WALLET_SETUP_COMMAND.format(mnemonic=mnemonic, binary=binary, account_name=account_name))
34
34
 
35
35
 
36
36
  def execute_tx(command: str, api_endpoint: str = config.stride.api_endpoint) -> TxResponse:
@@ -14,5 +14,5 @@ class TxResponse:
14
14
  tx_response: dict | None = ...
15
15
  raw_log: str | None = ...
16
16
 
17
- def restore_wallet(wallet_name: str = 'wallet', mnemonic_var: str = 'MNEMONIC', binary: str = 'strided'): ...
17
+ def restore_wallet(account_name: str = 'wallet', mnemonic_var: str = 'MNEMONIC', binary: str = 'strided'): ...
18
18
  def execute_tx(command: str, api_endpoint: str = ...) -> TxResponse: ...
@@ -12,7 +12,7 @@ publicsheets@stride-nodes.iam.gserviceaccount.com can access will be readable by
12
12
 
13
13
  import json
14
14
  import warnings
15
- from typing import Dict, List, Tuple
15
+ from typing import Any, Dict, List, Tuple
16
16
 
17
17
  import gspread # type: ignore
18
18
  import pandas as pd
@@ -139,7 +139,40 @@ class GoogleSheetsClient:
139
139
  # Write the new values with update (which will override an existing sheet).
140
140
  with warnings.catch_warnings():
141
141
  warnings.filterwarnings("ignore")
142
- worksheet.update(values=columns + rows, range_name=range_name)
142
+ worksheet.update(values=columns + rows, range_name=range_name) # type: ignore
143
+
144
+ def write_cell(
145
+ self,
146
+ value: List[List[Any]],
147
+ range_name: str,
148
+ sheet_id: str,
149
+ sheet_name: str,
150
+ ) -> None:
151
+ """
152
+ Writes a value to a specified Google Sheet.
153
+
154
+ Please note: value must be a list of lists, where each inner list represents a row.
155
+ If you're only passing in a value for one cell, then `value=[[your_value]]`.
156
+
157
+ Args:
158
+ value: The value to write in the sheet. This is a List of Lists.
159
+ range_name: The range to write the value to, e.g. "B3:C4"
160
+ sheet_id: The ID of the Sheet (found in the URL)
161
+ sheet_name: The name of the worksheet to write to
162
+
163
+ Raises:
164
+ gspread.exceptions.APIError: If there's an error accessing the sheet
165
+ """
166
+ # Fetch the worksheet if it exists, otherwise create a new worksheet.
167
+ spreadsheet = self.client.open_by_key(sheet_id)
168
+ try:
169
+ worksheet = spreadsheet.worksheet(sheet_name)
170
+ except WorksheetNotFound:
171
+ worksheet = spreadsheet.add_worksheet(title=sheet_name, rows=1, cols=1)
172
+
173
+ with warnings.catch_warnings():
174
+ warnings.filterwarnings("ignore")
175
+ worksheet.update(values=value, range_name=range_name)
143
176
 
144
177
  def reorder_sheet(
145
178
  self,
@@ -1,6 +1,7 @@
1
1
  import gspread
2
2
  import pandas as pd
3
3
  from strideutils.stride_config import get_env_or_raise as get_env_or_raise
4
+ from typing import Any
4
5
 
5
6
  SCOPES: tuple[str, str, str]
6
7
 
@@ -11,5 +12,6 @@ class GoogleSheetsClient:
11
12
  def __init__(self, cache_enabled: bool = False) -> None: ...
12
13
  def grab_sheet(self, sheet_id: str, sheet_name: str, columns_labeled: bool = True) -> pd.DataFrame: ...
13
14
  def write_sheet(self, df: pd.DataFrame, sheet_id: str, sheet_name: str) -> None: ...
15
+ def write_cell(self, value: list[list[Any]], range_name: str, sheet_id: str, sheet_name: str) -> None: ...
14
16
  def reorder_sheet(self, sheet_id: str, sheet_name: str, new_location: int = 0) -> None: ...
15
17
  def get_sheet_names(self, sheet_id: str) -> list[str]: ...
@@ -0,0 +1,183 @@
1
+ import datetime
2
+ import os
3
+ from dataclasses import dataclass
4
+ from typing import Callable, List, Optional, Union
5
+
6
+ from slack_sdk import WebClient
7
+ from slack_sdk.socket_mode import SocketModeClient
8
+ from slack_sdk.socket_mode.client import BaseSocketModeClient
9
+ from slack_sdk.socket_mode.request import SocketModeRequest
10
+ from slack_sdk.socket_mode.response import SocketModeResponse
11
+
12
+ from strideutils.stride_config import Environment as e
13
+ from strideutils.stride_config import get_env_or_raise
14
+
15
+
16
+ @dataclass
17
+ class SlackMessage:
18
+ timestamp: datetime.datetime
19
+ timestamp_id: str
20
+ thread_ts: Optional[str]
21
+ sender: str
22
+ text: str
23
+
24
+ @classmethod
25
+ def from_dict(cls, message: dict):
26
+ thread_ts = message.get("thread_ts", None)
27
+ return cls(
28
+ timestamp=datetime.datetime.fromtimestamp(float(message["ts"])),
29
+ timestamp_id=message["ts"],
30
+ thread_ts=thread_ts,
31
+ sender=message["user"],
32
+ text=message["text"],
33
+ )
34
+
35
+
36
+ class SlackClient:
37
+ """
38
+ Singleton client used to post messages to slack
39
+ """
40
+
41
+ _instance = None
42
+
43
+ def __new__(cls):
44
+ # Creates a new instance if one does not already exist
45
+ if cls._instance is None:
46
+ cls._instance = super(SlackClient, cls).__new__(cls)
47
+ return cls._instance
48
+
49
+ def __init__(self):
50
+ # Prevent re-initialization if a client has already been created
51
+ if hasattr(self, "client"):
52
+ return
53
+
54
+ self.api_token = get_env_or_raise(e.STRIDEBOT_API_TOKEN)
55
+ self.client = WebClient(token=self.api_token)
56
+ self.socket_mode_client: SocketModeClient | None = None
57
+
58
+ def setup_for_listening(self):
59
+ """
60
+ Sets up the client for listening to slack messages using SocketMode.
61
+ Without calling this function, a Socket Mode client will not be created.
62
+
63
+ We don't create the client in the constructor to avoid unnecessary overhead,
64
+ as not all processes will want to initiate socket connections.
65
+ """
66
+ self.app_token = get_env_or_raise(e.STRIDEBOT_APP_TOKEN)
67
+ socket_mode_client = SocketModeClient(app_token=self.app_token, web_client=self.client)
68
+ print("Socket mode client created")
69
+ self.socket_mode_client = socket_mode_client
70
+
71
+ def post_message(
72
+ self,
73
+ message: Union[str, List[str]],
74
+ channel: str,
75
+ botname: Optional[str] = None,
76
+ thread_ts: Optional[str] = None,
77
+ ):
78
+ """
79
+ Posts a slack message to the given channel
80
+ If the message is an array, this will post each element of the array as a thread
81
+
82
+ Returns the thread_ts to allow chaining messages
83
+ """
84
+ # Allow optional channel overrides via an environment variable
85
+ channel = os.environ.get(e.SLACK_CHANNEL_OVERRIDE, default=channel)
86
+
87
+ # Listify
88
+ messages = [message] if type(message) is str else message
89
+
90
+ # Post each message, threading with the previous
91
+ thread_ts = None or thread_ts
92
+ for msg in messages:
93
+ response = self.client.chat_postMessage(
94
+ channel=channel,
95
+ text=msg,
96
+ thread_ts=thread_ts,
97
+ username=botname,
98
+ )
99
+ if thread_ts is None: # API advises to use parent IDs instead of child's
100
+ thread_ts = response["ts"]
101
+
102
+ return thread_ts
103
+
104
+ def upload_file(self, file_name: str, content: str) -> str:
105
+ """
106
+ This uploads a file and returns a link that can be used to embed a file in slack
107
+ Ex:
108
+ url = upload_file("test.txt", "Hello World")
109
+ post_msg(f"<{url}|This is a file>", channel="#alerts-debug")
110
+ """
111
+ slack_file = self.client.files_upload_v2(filename=file_name, content=content)
112
+ file_link = slack_file["file"]["permalink"]
113
+ return file_link
114
+
115
+ def listen_to_channel(
116
+ self, message_callback: Callable[[BaseSocketModeClient, dict], None], target_channel: str
117
+ ) -> None:
118
+ """
119
+ Starts a listener for slack messages in the specified channel using SocketMode.
120
+ Calls `message_callback` with the event data when a message is received.
121
+ """
122
+ if not self.socket_mode_client:
123
+ self.setup_for_listening()
124
+ assert self.socket_mode_client is not None, "Failed to initialize socket mode client"
125
+
126
+ def handle_message(client: BaseSocketModeClient, req: SocketModeRequest):
127
+ if req.type == "events_api":
128
+ # ack the request immediately
129
+ response = SocketModeResponse(envelope_id=req.envelope_id)
130
+ if req.payload["event"]["channel"] == target_channel:
131
+ client.send_socket_mode_response(response)
132
+ message_callback(client, req.payload["event"])
133
+
134
+ self.socket_mode_client.socket_mode_request_listeners.append(handle_message)
135
+ self.socket_mode_client.connect()
136
+
137
+ def get_prior_messages_in_channel(self, channel: str, limit: int = 200, max_age=-1) -> List[List[SlackMessage]]:
138
+ """
139
+ Fetch the most recent messages from a Slack channel.
140
+ Args:
141
+ channel (str): ID of the Slack channel.
142
+ limit (int): Maximum number of messages to fetch (default: 100).
143
+ max_age (int): Maximum age in seconds of messages to fetch (default: -1, fetch all messages).
144
+ Returns:
145
+ List[List[SlackMessages]]: A list of list of message objects.
146
+ Each element in the array is a list of messages in that thread.
147
+ """
148
+ # fetch message history
149
+ response = self.client.conversations_history(channel=channel, limit=limit)
150
+
151
+ raw_messages = response.get("messages", [])
152
+ channel_messages: list[list[SlackMessage]] = []
153
+ now_time = datetime.datetime.now()
154
+ for raw_message in raw_messages:
155
+ # check if message is too old
156
+ if max_age > 0:
157
+ message_time = datetime.datetime.fromtimestamp(float(raw_message["ts"]))
158
+ if (now_time - message_time).total_seconds() > max_age:
159
+ continue
160
+
161
+ if 'thread_ts' in raw_message:
162
+ # make sure we haven't processed this thread already.
163
+ # This happens when multiple messages from the same thread are returned in `response`.
164
+ # After we process the first message, because we call `conversations_replies` below,
165
+ # we will have already processed the whole thread.
166
+ thread_ts = raw_message['thread_ts']
167
+ if thread_ts in [msg[0].thread_ts for msg in channel_messages]:
168
+ continue
169
+
170
+ # this is a thread, fetch the full thread
171
+ thread_replies = self.client.conversations_replies(channel=channel, ts=thread_ts)
172
+ thread_msgs = []
173
+ for reply in thread_replies["messages"]:
174
+ thread_msg = SlackMessage.from_dict(reply)
175
+ thread_msgs.append(thread_msg)
176
+ channel_messages.append(thread_msgs)
177
+
178
+ else:
179
+ # this is not a thread
180
+ slack_msg = SlackMessage.from_dict(raw_message)
181
+ channel_messages.append([slack_msg])
182
+
183
+ return channel_messages
@@ -0,0 +1,29 @@
1
+ import datetime
2
+ from _typeshed import Incomplete
3
+ from dataclasses import dataclass
4
+ from slack_sdk.socket_mode.client import BaseSocketModeClient
5
+ from strideutils.stride_config import get_env_or_raise as get_env_or_raise
6
+ from typing import Callable
7
+
8
+ @dataclass
9
+ class SlackMessage:
10
+ timestamp: datetime.datetime
11
+ timestamp_id: str
12
+ thread_ts: str | None
13
+ sender: str
14
+ text: str
15
+ @classmethod
16
+ def from_dict(cls, message: dict): ...
17
+
18
+ class SlackClient:
19
+ def __new__(cls): ...
20
+ api_token: Incomplete
21
+ client: Incomplete
22
+ socket_mode_client: Incomplete
23
+ def __init__(self) -> None: ...
24
+ app_token: Incomplete
25
+ def setup_for_listening(self) -> None: ...
26
+ def post_message(self, message: str | list[str], channel: str, botname: str | None = None, thread_ts: str | None = None): ...
27
+ def upload_file(self, file_name: str, content: str) -> str: ...
28
+ def listen_to_channel(self, message_callback: Callable[[BaseSocketModeClient, dict], None], target_channel: str) -> None: ...
29
+ def get_prior_messages_in_channel(self, channel: str, limit: int = 200, max_age: int = -1) -> list[list[SlackMessage]]: ...
@@ -2,10 +2,9 @@
2
2
  This file loads and provide an object `config` to access all secrets, configs, and chain info
3
3
  """
4
4
 
5
- import itertools
6
5
  import os
7
6
  from dataclasses import dataclass, field, fields
8
- from typing import Any, Dict, List, Optional, Union, cast
7
+ from typing import Any, Dict, List, Optional, cast
9
8
 
10
9
  import yaml
11
10
  from dotenv import load_dotenv
@@ -33,6 +32,7 @@ class Environment:
33
32
  TWILIO_API_TOKEN = "TWILIO_API_TOKEN"
34
33
 
35
34
  STRIDEBOT_API_TOKEN = "STRIDEBOT_API_TOKEN"
35
+ STRIDEBOT_APP_TOKEN = "STRIDEBOT_APP_TOKEN"
36
36
  SLACK_CHANNEL_OVERRIDE = "SLACK_CHANNEL_OVERRIDE"
37
37
 
38
38
  PUBLICSHEETS_AUTH = "PUBLICSHEETS_AUTH"
@@ -191,6 +191,10 @@ class Config(ConfigObj):
191
191
  def app_chains(self) -> List[ChainConfig]:
192
192
  return [app_chain for app_chain in self._app_chains.values()]
193
193
 
194
+ @property
195
+ def chains(self) -> List[ChainConfig]:
196
+ return self.host_zones + self.app_chains
197
+
194
198
  @property
195
199
  def host_zone_names(self) -> List[str]:
196
200
  return [host_zone for host_zone in self._host_zones.keys()]
@@ -199,55 +203,33 @@ class Config(ConfigObj):
199
203
  def app_chain_names(self) -> List[str]:
200
204
  return [app_chain for app_chain in self._app_chains.keys()]
201
205
 
202
- def _create_mappings(self) -> None:
203
- """
204
- Create a reverse map from attributes to ChainConfig if id, ticker, and denom are set.
205
- """
206
- name_to_zone = ConfigDict()
207
- id_to_zone = ConfigDict()
208
- ticker_to_zone = ConfigDict()
209
- denom_to_zone = ConfigDict()
210
- denom_on_stride_to_zone = ConfigDict()
211
-
212
- # Map host zones and app chains.
213
- for chain_info in itertools.chain(self._host_zones.values(), self._app_chains.values()):
214
- try:
215
- name_to_zone[chain_info.name] = chain_info
216
- except AttributeError:
217
- pass
218
-
219
- try:
220
- id_to_zone[chain_info.id] = chain_info
221
- except AttributeError:
222
- pass
223
-
224
- try:
225
- ticker_to_zone[chain_info.ticker] = chain_info
226
- ticker_to_zone[chain_info.ticker.upper()] = chain_info
227
- ticker_to_zone[chain_info.ticker.lower()] = chain_info
228
- ticker_to_zone[chain_info.denom] = chain_info
229
- except AttributeError:
230
- pass
231
-
232
- try:
233
- denom_to_zone[chain_info.denom] = chain_info
234
- except AttributeError:
235
- pass
236
-
237
- if isinstance(chain_info, HostChainConfig):
238
- try:
239
- denom_on_stride_to_zone[chain_info.denom_on_stride] = chain_info
240
- except AttributeError:
241
- pass
242
-
243
- self._name_to_zone = name_to_zone
244
- self._id_to_zone = id_to_zone
245
- self._ticker_to_zone = ticker_to_zone
246
- self._denom_to_zone = denom_to_zone
247
- self._denom_on_stride_to_zone = denom_on_stride_to_zone
248
-
249
- def __post_init__(self):
250
- self._create_mappings()
206
+ def _evaluate_chain_match(
207
+ self,
208
+ chain: ChainConfig,
209
+ name: Optional[str],
210
+ id: Optional[str],
211
+ ticker: Optional[str],
212
+ denom: Optional[str],
213
+ denom_on_stride: Optional[str],
214
+ ) -> bool:
215
+ if name is not None:
216
+ return chain.name == name
217
+
218
+ if id is not None:
219
+ return chain.id == id
220
+
221
+ if ticker is not None:
222
+ chain_tickers = [chain.ticker, chain.ticker.upper(), chain.ticker.lower(), chain.denom]
223
+ return ticker in chain_tickers
224
+
225
+ if denom is not None:
226
+ return chain.denom == denom
227
+
228
+ if denom_on_stride is not None:
229
+ if isinstance(chain, HostChainConfig):
230
+ return chain.denom_on_stride == denom_on_stride
231
+
232
+ return False
251
233
 
252
234
  def get_chain(
253
235
  self,
@@ -256,7 +238,7 @@ class Config(ConfigObj):
256
238
  ticker: Optional[str] = None,
257
239
  denom: Optional[str] = None,
258
240
  denom_on_stride: Optional[str] = None,
259
- ) -> Union[ChainConfig, StrideChainConfig, HostChainConfig]:
241
+ ) -> ChainConfig:
260
242
  """
261
243
  Fetch info about a host zone by name, id, ticker, denom, or token hash.
262
244
 
@@ -274,22 +256,18 @@ class Config(ConfigObj):
274
256
  ):
275
257
  return self.stride
276
258
 
277
- try:
278
- if name:
279
- return self._name_to_zone[name]
280
- elif id:
281
- return self._id_to_zone[id]
282
- elif ticker:
283
- return self._ticker_to_zone[ticker]
284
- elif denom:
285
- return self._denom_to_zone[denom]
286
- elif denom_on_stride:
287
- return self._denom_on_stride_to_zone[denom_on_stride]
288
- except KeyError:
289
- raise KeyError("No chain found for the given parameter")
290
-
291
- # This line should never be reached due to the initial check and the try-except block.
292
- raise KeyError("Unexpected error in get_chain method")
259
+ for chain in self.chains:
260
+ if self._evaluate_chain_match(
261
+ chain=chain,
262
+ name=name,
263
+ id=id,
264
+ ticker=ticker,
265
+ denom=denom,
266
+ denom_on_stride=denom_on_stride,
267
+ ):
268
+ return chain
269
+
270
+ raise KeyError("No chain found for the given parameter")
293
271
 
294
272
  def get_host_chain(
295
273
  self,
@@ -306,16 +284,21 @@ class Config(ConfigObj):
306
284
  KeyError if a valid host chain doesn't exist or if multiple parameters are specified
307
285
  TypeError if the found chain is not a host chain
308
286
  """
309
- chain_config = self.get_chain(
310
- name=name,
311
- id=id,
312
- ticker=ticker,
313
- denom=denom,
314
- denom_on_stride=denom_on_stride,
315
- )
316
- if not isinstance(chain_config, HostChainConfig):
317
- raise TypeError(f"Chain {chain_config.name} is not a host zone")
318
- return chain_config
287
+ if sum(map(bool, [name, id, ticker, denom, denom_on_stride])) != 1:
288
+ raise KeyError("Exactly one of name, id, ticker, denom, or token hash must be specified.")
289
+
290
+ for chain in self.host_zones:
291
+ if self._evaluate_chain_match(
292
+ chain=chain,
293
+ name=name,
294
+ id=id,
295
+ ticker=ticker,
296
+ denom=denom,
297
+ denom_on_stride=denom_on_stride,
298
+ ):
299
+ return chain
300
+
301
+ raise KeyError("No host chain found for the given parameter")
319
302
 
320
303
 
321
304
  def _load_raw_config() -> dict[str, Any]:
@@ -14,6 +14,7 @@ class Environment:
14
14
  TWILIO_ACCOUNT_ID = ...
15
15
  TWILIO_API_TOKEN = ...
16
16
  STRIDEBOT_API_TOKEN = ...
17
+ STRIDEBOT_APP_TOKEN = ...
17
18
  SLACK_CHANNEL_OVERRIDE = ...
18
19
  PUBLICSHEETS_AUTH = ...
19
20
  COINGECKO_API_TOKEN = ...
@@ -80,11 +81,12 @@ class Config(ConfigObj):
80
81
  @property
81
82
  def app_chains(self) -> list[ChainConfig]: ...
82
83
  @property
84
+ def chains(self) -> list[ChainConfig]: ...
85
+ @property
83
86
  def host_zone_names(self) -> list[str]: ...
84
87
  @property
85
88
  def app_chain_names(self) -> list[str]: ...
86
- def __post_init__(self) -> None: ...
87
- def get_chain(self, name: str | None = None, id: str | None = None, ticker: str | None = None, denom: str | None = None, denom_on_stride: str | None = None) -> ChainConfig | StrideChainConfig | HostChainConfig: ...
89
+ def get_chain(self, name: str | None = None, id: str | None = None, ticker: str | None = None, denom: str | None = None, denom_on_stride: str | None = None) -> ChainConfig: ...
88
90
  def get_host_chain(self, name: str | None = None, id: str | None = None, ticker: str | None = None, denom: str | None = None, denom_on_stride: str | None = None) -> HostChainConfig: ...
89
91
 
90
92
  config: Config
@@ -15,5 +15,5 @@ class TxResponse:
15
15
  tx_response: dict | None = ...
16
16
  raw_log: str | None = ...
17
17
 
18
- def restore_wallet(wallet_name: str = 'wallet', mnemonic_var: str = 'MNEMONIC', binary: str = 'strided'): ...
18
+ def restore_wallet(account_name: str = 'wallet', mnemonic_var: str = 'MNEMONIC', binary: str = 'strided'): ...
19
19
  def execute_tx(command: str, api_endpoint: str = ...) -> TxResponse: ...
@@ -1,3 +1,5 @@
1
+ from typing import Any
2
+
1
3
  import gspread
2
4
  import pandas as pd
3
5
 
@@ -12,5 +14,6 @@ class GoogleSheetsClient:
12
14
  def __init__(self, cache_enabled: bool = False) -> None: ...
13
15
  def grab_sheet(self, sheet_id: str, sheet_name: str, columns_labeled: bool = True) -> pd.DataFrame: ...
14
16
  def write_sheet(self, df: pd.DataFrame, sheet_id: str, sheet_name: str) -> None: ...
17
+ def write_cell(self, value: list[list[Any]], range_name: str, sheet_id: str, sheet_name: str) -> None: ...
15
18
  def reorder_sheet(self, sheet_id: str, sheet_name: str, new_location: int = 0) -> None: ...
16
19
  def get_sheet_names(self, sheet_id: str) -> list[str]: ...
@@ -0,0 +1,37 @@
1
+ import datetime
2
+ from dataclasses import dataclass
3
+ from typing import Callable
4
+
5
+ from _typeshed import Incomplete
6
+ from slack_sdk.socket_mode.client import BaseSocketModeClient
7
+
8
+ from strideutils.stride_config import get_env_or_raise as get_env_or_raise
9
+
10
+ @dataclass
11
+ class SlackMessage:
12
+ timestamp: datetime.datetime
13
+ timestamp_id: str
14
+ thread_ts: str | None
15
+ sender: str
16
+ text: str
17
+ @classmethod
18
+ def from_dict(cls, message: dict): ...
19
+
20
+ class SlackClient:
21
+ def __new__(cls): ...
22
+ api_token: Incomplete
23
+ client: Incomplete
24
+ socket_mode_client: Incomplete
25
+ def __init__(self) -> None: ...
26
+ app_token: Incomplete
27
+ def setup_for_listening(self) -> None: ...
28
+ def post_message(
29
+ self, message: str | list[str], channel: str, botname: str | None = None, thread_ts: str | None = None
30
+ ): ...
31
+ def upload_file(self, file_name: str, content: str) -> str: ...
32
+ def listen_to_channel(
33
+ self, message_callback: Callable[[BaseSocketModeClient, dict], None], target_channel: str
34
+ ) -> None: ...
35
+ def get_prior_messages_in_channel(
36
+ self, channel: str, limit: int = 200, max_age: int = -1
37
+ ) -> list[list[SlackMessage]]: ...
@@ -14,6 +14,7 @@ class Environment:
14
14
  TWILIO_ACCOUNT_ID = ...
15
15
  TWILIO_API_TOKEN = ...
16
16
  STRIDEBOT_API_TOKEN = ...
17
+ STRIDEBOT_APP_TOKEN = ...
17
18
  SLACK_CHANNEL_OVERRIDE = ...
18
19
  PUBLICSHEETS_AUTH = ...
19
20
  COINGECKO_API_TOKEN = ...
@@ -79,10 +80,11 @@ class Config(ConfigObj):
79
80
  @property
80
81
  def app_chains(self) -> list[ChainConfig]: ...
81
82
  @property
83
+ def chains(self) -> list[ChainConfig]: ...
84
+ @property
82
85
  def host_zone_names(self) -> list[str]: ...
83
86
  @property
84
87
  def app_chain_names(self) -> list[str]: ...
85
- def __post_init__(self) -> None: ...
86
88
  def get_chain(
87
89
  self,
88
90
  name: str | None = None,
@@ -90,7 +92,7 @@ class Config(ConfigObj):
90
92
  ticker: str | None = None,
91
93
  denom: str | None = None,
92
94
  denom_on_stride: str | None = None,
93
- ) -> ChainConfig | StrideChainConfig | HostChainConfig: ...
95
+ ) -> ChainConfig: ...
94
96
  def get_host_chain(
95
97
  self,
96
98
  name: str | None = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: strideutils
3
- Version: 0.8.4
3
+ Version: 0.8.6
4
4
  Summary: Utility functions for monitoring the Stride blockchain
5
5
  Author-email: Stride Labs <hello@stridelabs.co>
6
6
  License: MIT License
@@ -222,13 +222,13 @@ class TestParseConfig:
222
222
  assert isinstance(host_chain, HostChainConfig)
223
223
 
224
224
  # Test looking up stride (should fail)
225
- with pytest.raises(TypeError, match="Chain stride is not a host zone"):
225
+ with pytest.raises(KeyError, match="No host chain found for the given parameter"):
226
226
  config.get_host_chain(name="stride")
227
227
 
228
228
  # Test looking up an app chain (should fail)
229
- with pytest.raises(TypeError, match="Chain neutron is not a host zone"):
229
+ with pytest.raises(KeyError, match="No host chain found for the given parameter"):
230
230
  config.get_host_chain(name="neutron")
231
231
 
232
232
  # Test looking up non-existent chain
233
- with pytest.raises(KeyError, match="No chain found for the given parameter"):
234
- config.get_host_chain(name="non-existent")
233
+ with pytest.raises(KeyError, match="No host chain found for the given parameter"):
234
+ config.get_host_chain(name="no-exist")
@@ -1,147 +0,0 @@
1
- import os
2
- import traceback
3
- from typing import List, Optional, Union
4
-
5
- from slack_sdk import WebClient
6
- from slack_sdk.errors import SlackApiError
7
-
8
- from strideutils.stride_config import Environment as e
9
- from strideutils.stride_config import config, get_env_or_raise
10
-
11
-
12
- class SlackClient:
13
- """
14
- Singleton client used to post messages to slack
15
- """
16
-
17
- _instance = None
18
-
19
- def __new__(cls):
20
- # Creates a new instance if one does not already exist
21
- if cls._instance is None:
22
- cls._instance = super(SlackClient, cls).__new__(cls)
23
- return cls._instance
24
-
25
- def __init__(self):
26
- # Prevent re-initialization if a client has already been created
27
- if hasattr(self, "client"):
28
- return
29
-
30
- self.api_token = get_env_or_raise(e.STRIDEBOT_API_TOKEN)
31
- self.client = WebClient(token=self.api_token)
32
-
33
- def post_message(
34
- self,
35
- message: Union[str, List[str]],
36
- channel: str,
37
- botname: Optional[str] = None,
38
- thread_ts: Optional[str] = None,
39
- ):
40
- """
41
- Posts a slack message to the given channel
42
- If the message is an array, this will post each element of the array as a thread
43
-
44
- Returns the thread_ts to allow chaining messages
45
- """
46
- # Allow optional channel overrides via an environment variable
47
- channel = os.environ.get(e.SLACK_CHANNEL_OVERRIDE, default=channel)
48
-
49
- # Listify
50
- messages = [message] if type(message) is str else message
51
-
52
- # Post each message, threading with the previous
53
- thread_ts = None or thread_ts
54
- for msg in messages:
55
- response = self.client.chat_postMessage(
56
- channel=channel,
57
- text=msg,
58
- thread_ts=thread_ts,
59
- username=botname,
60
- )
61
- if thread_ts is None: # API advises to use parent IDs instead of child's
62
- thread_ts = response["ts"]
63
-
64
- return thread_ts
65
-
66
- def upload_file(self, file_name: str, content: str) -> str:
67
- """
68
- This uploads a file and returns a link that can be used to embed a file in slack
69
- Ex:
70
- url = upload_file("test.txt", "Hello World")
71
- post_msg(f"<{url}|This is a file>", channel="#alerts-debug")
72
- """
73
- slack_file = self.client.files_upload_v2(filename=file_name, content=content)
74
- file_link = slack_file["file"]["permalink"]
75
- return file_link
76
-
77
-
78
- # TODO: Remove after all launchpad instances are removed
79
- def post_msg(
80
- txt: Union[str, List[str]],
81
- channel: str,
82
- botname: Optional[str] = None,
83
- unfurl_links: bool = False,
84
- unfurl_media: bool = False,
85
- thread_ts: Optional[str] = None,
86
- ):
87
- """
88
- DEPRECATED: Use slack client instead
89
-
90
- Posts a slack message to the given channel, e.g.
91
-
92
- post_msg('hello world', 'general')
93
-
94
- if txt is an array, this will post each element of the array as a thread
95
-
96
- Args:
97
- username: Rename stridebot
98
- """
99
- token = get_env_or_raise(e.STRIDEBOT_API_TOKEN)
100
- client = WebClient(token=token)
101
- channel = config.slack_channels.get(channel.replace('#', ''), channel)
102
- channel = os.environ.get('SLACK_CHANNEL_OVERRIDE', default=channel)
103
-
104
- # Listify
105
- messages = [txt] if type(txt) is str else txt
106
-
107
- # thread ts keeps track of the latest thread ID so we can keep replying in-thread
108
- thread_ts = None or thread_ts
109
- for msg in messages:
110
- try:
111
- response = client.chat_postMessage(
112
- channel=channel,
113
- text=msg,
114
- thread_ts=thread_ts,
115
- username=botname,
116
- unfurl_links=unfurl_links,
117
- unfurl_media=unfurl_media,
118
- )
119
- if thread_ts is None: # API advises to use parent IDs instead of child's
120
- thread_ts = response['ts']
121
- except SlackApiError as error:
122
- print("Error sending message: ", error)
123
- raise
124
- return thread_ts
125
-
126
-
127
- def upload_file(file_name: str, content: str) -> str:
128
- """
129
- DEPRECATED: Use slack client instead
130
-
131
- This uploads a file called "file_name" with the string contents "content"
132
-
133
- This will return a link that can be used to embed a file in slack.
134
- For example:
135
- url = upload_file("test.txt", "Hello World")
136
- post_msg(f"<{url}|This is a file>", channel="#alerts-debug")
137
- """
138
- token = get_env_or_raise(e.STRIDEBOT_API_TOKEN)
139
- client = WebClient(token=token)
140
- try:
141
- slack_file = client.files_upload_v2(filename=file_name, content=content)
142
- except Exception as error:
143
- print(f"Error uploading file to slack: {error}")
144
- traceback.print_exc()
145
- raise ValueError(f"Error uploading file to slack: {error}")
146
- file_link = slack_file['file']['permalink']
147
- return file_link
@@ -1,13 +0,0 @@
1
- from _typeshed import Incomplete
2
- from strideutils.stride_config import config as config, get_env_or_raise as get_env_or_raise
3
-
4
- class SlackClient:
5
- def __new__(cls): ...
6
- api_token: Incomplete
7
- client: Incomplete
8
- def __init__(self) -> None: ...
9
- def post_message(self, message: str | list[str], channel: str, botname: str | None = None, thread_ts: str | None = None): ...
10
- def upload_file(self, file_name: str, content: str) -> str: ...
11
-
12
- def post_msg(txt: str | list[str], channel: str, botname: str | None = None, unfurl_links: bool = False, unfurl_media: bool = False, thread_ts: str | None = None): ...
13
- def upload_file(file_name: str, content: str) -> str: ...
@@ -1,24 +0,0 @@
1
- from _typeshed import Incomplete
2
-
3
- from strideutils.stride_config import config as config
4
- from strideutils.stride_config import get_env_or_raise as get_env_or_raise
5
-
6
- class SlackClient:
7
- def __new__(cls): ...
8
- api_token: Incomplete
9
- client: Incomplete
10
- def __init__(self) -> None: ...
11
- def post_message(
12
- self, message: str | list[str], channel: str, botname: str | None = None, thread_ts: str | None = None
13
- ): ...
14
- def upload_file(self, file_name: str, content: str) -> str: ...
15
-
16
- def post_msg(
17
- txt: str | list[str],
18
- channel: str,
19
- botname: str | None = None,
20
- unfurl_links: bool = False,
21
- unfurl_media: bool = False,
22
- thread_ts: str | None = None,
23
- ): ...
24
- def upload_file(file_name: str, content: str) -> str: ...
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes