helius-python 0.5.0__tar.gz → 0.5.2__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 (50) hide show
  1. {helius_python-0.5.0 → helius_python-0.5.2}/PKG-INFO +1 -1
  2. {helius_python-0.5.0 → helius_python-0.5.2}/pyproject.toml +1 -1
  3. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/laserstream/websockets.py +33 -22
  4. {helius_python-0.5.0 → helius_python-0.5.2}/test_examples.py +83 -22
  5. {helius_python-0.5.0 → helius_python-0.5.2}/tests/unit/lasterstream/test_websockets.py +70 -5
  6. {helius_python-0.5.0 → helius_python-0.5.2}/.editorconfig +0 -0
  7. {helius_python-0.5.0 → helius_python-0.5.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  8. {helius_python-0.5.0 → helius_python-0.5.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  9. {helius_python-0.5.0 → helius_python-0.5.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  10. {helius_python-0.5.0 → helius_python-0.5.2}/.github/workflows/python-package.yml +0 -0
  11. {helius_python-0.5.0 → helius_python-0.5.2}/.github/workflows/python-publish.yml +0 -0
  12. {helius_python-0.5.0 → helius_python-0.5.2}/.gitignore +0 -0
  13. {helius_python-0.5.0 → helius_python-0.5.2}/AGENTS.md +0 -0
  14. {helius_python-0.5.0 → helius_python-0.5.2}/CLAUDE.md +0 -0
  15. {helius_python-0.5.0 → helius_python-0.5.2}/CONTRIBUTING.md +0 -0
  16. {helius_python-0.5.0 → helius_python-0.5.2}/LICENSE +0 -0
  17. {helius_python-0.5.0 → helius_python-0.5.2}/README.md +0 -0
  18. {helius_python-0.5.0 → helius_python-0.5.2}/TODO.md +0 -0
  19. {helius_python-0.5.0 → helius_python-0.5.2}/examples/laserstream/websocket_logs.py +0 -0
  20. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.2/examples/rpc}/address_transactions.py +0 -0
  21. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.2/examples/rpc}/address_transfers.py +0 -0
  22. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.2/examples/rpc}/block_explorer.py +0 -0
  23. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.2/examples/rpc}/devnet_airdrop.py +0 -0
  24. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.2/examples/rpc}/network_status.py +0 -0
  25. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.2/examples/rpc}/priority_fees.py +0 -0
  26. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.2/examples/rpc}/stake_overview.py +0 -0
  27. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.2/examples/rpc}/token_inspector.py +0 -0
  28. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.2/examples/rpc}/transaction_inspector.py +0 -0
  29. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.2/examples/rpc}/wallet_tracker.py +0 -0
  30. {helius_python-0.5.0 → helius_python-0.5.2}/examples/webhooks/webhook_crud.py +0 -0
  31. {helius_python-0.5.0 → helius_python-0.5.2}/examples/webhooks/webhook_receiver.py +0 -0
  32. {helius_python-0.5.0 → helius_python-0.5.2}/requirements.txt +0 -0
  33. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/__init__.py +0 -0
  34. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/admin/__init__.py +0 -0
  35. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/admin/admin.py +0 -0
  36. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/rpc/__init__.py +0 -0
  37. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/rpc/client.py +0 -0
  38. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/rpc/models.py +0 -0
  39. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/utils/__init__.py +0 -0
  40. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/utils/json_rpc_request.py +0 -0
  41. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/webhooks/__init__.py +0 -0
  42. {helius_python-0.5.0 → helius_python-0.5.2}/src/helius/webhooks/webhooks.py +0 -0
  43. {helius_python-0.5.0 → helius_python-0.5.2}/tests/fixtures/account.json +0 -0
  44. {helius_python-0.5.0 → helius_python-0.5.2}/tests/fixtures/supply.json +0 -0
  45. {helius_python-0.5.0 → helius_python-0.5.2}/tests/unit/admin/test_admin.py +0 -0
  46. {helius_python-0.5.0 → helius_python-0.5.2}/tests/unit/solana_rpc/test_client.py +0 -0
  47. {helius_python-0.5.0 → helius_python-0.5.2}/tests/unit/solana_rpc/test_models.py +0 -0
  48. {helius_python-0.5.0 → helius_python-0.5.2}/tests/unit/utils/test_json_rpc_request.py +0 -0
  49. {helius_python-0.5.0 → helius_python-0.5.2}/tests/unit/webhooks/test_webhook.py +0 -0
  50. {helius_python-0.5.0 → helius_python-0.5.2}/tests/unit/webhooks/test_webhooks_api_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: helius-python
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Typed Python client for the Helius API
5
5
  Project-URL: Homepage, https://github.com/markosnarinian/helius-python
6
6
  Project-URL: Issues, https://github.com/markosnarinian/helius-python/issues
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
10
10
 
11
11
  [project]
12
12
  name = "helius-python"
13
- version = "0.5.0"
13
+ version = "0.5.2"
14
14
  authors = [
15
15
  { name="Markos Narinian", email="manarinian@gmail.com" },
16
16
  ]
@@ -132,6 +132,10 @@ class WebSocketClient:
132
132
  base_url="wss://mainnet.helius-rpc.com",
133
133
  api_key: str | None = None,
134
134
  proxy: str | None = None,
135
+ open_timeout: float | None = 10,
136
+ ping_interval: float | None = 20,
137
+ ping_timeout: float | None = 20,
138
+ close_timeout: float | None = 10,
135
139
  ):
136
140
 
137
141
  base_url = base_url
@@ -144,7 +148,14 @@ class WebSocketClient:
144
148
  if not api_key:
145
149
  raise ValueError("No API key provided.")
146
150
  uri = httpx.URL(base_url).copy_with(path="/", params={"api-key": api_key})
147
- self._websocket = connect(str(uri), proxy=proxy)
151
+ self._websocket = connect(
152
+ str(uri),
153
+ proxy=proxy,
154
+ open_timeout=open_timeout,
155
+ ping_interval=ping_interval,
156
+ ping_timeout=ping_timeout,
157
+ close_timeout=close_timeout,
158
+ )
148
159
 
149
160
  def close(self):
150
161
  self._websocket.close()
@@ -163,6 +174,27 @@ class WebSocketClient:
163
174
  response = self._websocket.recv()
164
175
  return json.loads(response)
165
176
 
177
+ def receive(self) -> tuple[dict | None, Notification, int]:
178
+ response = json.loads(self._websocket.recv())
179
+ model = self.MODELS[response["method"]]
180
+ result = response["params"]["result"]
181
+ subscription = response["params"]["subscription"]
182
+ if isinstance(result, dict):
183
+ context = result.get("context")
184
+ value = result.get("value")
185
+ else:
186
+ context, value = None, None
187
+ if value is not None:
188
+ notification = model.model_validate(value)
189
+ else:
190
+ notification = model.model_validate(result)
191
+ return context, notification, subscription
192
+
193
+ def listen(self):
194
+ while True:
195
+ context, notification, subscription = self.receive()
196
+ yield context, notification, subscription
197
+
166
198
  def _unsubscribe(self, subscription_type, subscription) -> bool:
167
199
  request = (
168
200
  JsonRpcRequest(method=f"{subscription_type}Unsubscribe")
@@ -376,24 +408,3 @@ class WebSocketClient:
376
408
 
377
409
  def vote_unsubscribe(self, subscription) -> bool:
378
410
  return self._unsubscribe("vote", subscription)
379
-
380
- def receive(self) -> tuple[dict | None, Notification, int]:
381
- response = json.loads(self._websocket.recv())
382
- model = self.MODELS[response["method"]]
383
- result = response["params"]["result"]
384
- subscription = response["params"]["subscription"]
385
- if isinstance(result, dict):
386
- context = result.get("context")
387
- value = result.get("value")
388
- else:
389
- context, value = None, None
390
- if value is not None:
391
- notification = model.model_validate(value)
392
- else:
393
- notification = model.model_validate(result)
394
- return context, notification, subscription
395
-
396
- def listen(self):
397
- while True:
398
- context, notification, subscription = self.receive()
399
- yield context, notification, subscription
@@ -9,13 +9,15 @@ Usage:
9
9
  .venv/bin/python test_examples.py
10
10
 
11
11
  The runner expects `HELIUS_API_KEY` to be available in the environment or in
12
- `.env`, matching the examples themselves. Some Helius endpoints are plan-gated
13
- or network-gated; those failures are reported as "external" instead of as
14
- example runtime bugs.
12
+ `.env`, matching the examples themselves. Missing/unauthorized API key failures
13
+ are reported as "auth" instead of as example runtime bugs. Some Helius endpoints
14
+ are plan-gated or network-gated; other failures in that class are reported as
15
+ "external".
15
16
  """
16
17
 
17
18
  from __future__ import annotations
18
19
 
20
+ import argparse
19
21
  import os
20
22
  import subprocess
21
23
  import sys
@@ -28,6 +30,7 @@ USE_COLOR = "NO_COLOR" not in os.environ
28
30
 
29
31
  GREEN = "\033[32m"
30
32
  YELLOW = "\033[33m"
33
+ ORANGE = "\033[38;5;208m"
31
34
  RED = "\033[31m"
32
35
  BOLD = "\033[1m"
33
36
  RESET = "\033[0m"
@@ -37,6 +40,16 @@ SMALL_MINT = "J5iyNuTa6zqqA62Xe4h1VBvcBW5CTSNNva3QPh8DU5RV"
37
40
  KNOWN_SIGNATURE = "eqRntqi1tjXv1zEGBM5btQGWoxWc73XXGDJXjxLE65Atj6T6qzNnJf5LyTbUoGXHS9TzeAnQniAre48SjcJft9f"
38
41
  DEVNET_ADDRESS = "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"
39
42
 
43
+ AUTH_FAILURE_MARKERS = (
44
+ "No API key provided.",
45
+ "HTTP 401",
46
+ "401 Unauthorized",
47
+ "HTTP 403",
48
+ "403 Forbidden",
49
+ "Unauthorized",
50
+ "Forbidden",
51
+ )
52
+
40
53
 
41
54
  @dataclass(frozen=True)
42
55
  class ExampleTest:
@@ -118,8 +131,6 @@ TESTS = [
118
131
  timeout=45,
119
132
  external_failure_markers=(
120
133
  "TimeoutError: timed out",
121
- "403 Forbidden",
122
- "HTTP 403",
123
134
  ),
124
135
  ),
125
136
  ExampleTest(
@@ -141,6 +152,41 @@ def color(text: str, ansi_color: str) -> str:
141
152
  return f"{ansi_color}{text}{RESET}"
142
153
 
143
154
 
155
+ def parse_args() -> argparse.Namespace:
156
+ parser = argparse.ArgumentParser(description=__doc__)
157
+ parser.add_argument(
158
+ "-v",
159
+ action="count",
160
+ default=0,
161
+ dest="verbose_count",
162
+ help="increase verbosity; use -v for level 1 or -vv for level 2",
163
+ )
164
+ parser.add_argument(
165
+ "--verbose",
166
+ type=int,
167
+ choices=(0, 1, 2),
168
+ default=None,
169
+ help=(
170
+ "0: only results and failure names; "
171
+ "1: include script output for non-passing tests; "
172
+ "2: include script output for all tests"
173
+ ),
174
+ )
175
+ args = parser.parse_args()
176
+ args.verbose = (
177
+ args.verbose if args.verbose is not None else min(args.verbose_count, 2)
178
+ )
179
+ return args
180
+
181
+
182
+ def output_preview(output: str) -> str:
183
+ lines = output.strip().splitlines()
184
+ preview = "\n".join(lines[:30])
185
+ if len(lines) > 30:
186
+ preview += f"\n... ({len(lines) - 30} more lines)"
187
+ return preview
188
+
189
+
144
190
  def run_example(test: ExampleTest) -> tuple[str, str]:
145
191
  env = os.environ.copy()
146
192
  env["PYTHONPATH"] = (
@@ -167,47 +213,60 @@ def run_example(test: ExampleTest) -> tuple[str, str]:
167
213
  output = result.stdout + result.stderr
168
214
  if result.returncode == 0:
169
215
  return "passed", output
216
+ if any(marker in output for marker in AUTH_FAILURE_MARKERS):
217
+ return "auth", output
170
218
  if any(marker in output for marker in test.external_failure_markers):
171
219
  return "external", output
172
220
  return "failed", output
173
221
 
174
222
 
175
223
  def main() -> int:
224
+ args = parse_args()
176
225
  passed: list[str] = []
226
+ auth: list[str] = []
177
227
  external: list[str] = []
178
228
  failed: list[tuple[str, str]] = []
179
229
 
180
230
  for test in TESTS:
181
- print(f"\n=== {test.name} ===", flush=True)
182
231
  status, output = run_example(test)
183
232
  if status == "passed":
184
233
  passed.append(test.name)
185
- print(color("PASS", GREEN + BOLD))
234
+ result = color("PASS", GREEN + BOLD)
235
+ elif status == "auth":
236
+ auth.append(test.name)
237
+ result = (
238
+ color("AUTH", ORANGE + BOLD)
239
+ + " - missing API key or endpoint is not authorized for this key"
240
+ )
186
241
  elif status == "external":
187
242
  external.append(test.name)
188
- print(
189
- color(
190
- "EXTERNAL",
191
- YELLOW + BOLD,
192
- )
193
- + ": endpoint, plan, or network prevented a live success"
243
+ result = (
244
+ color("EXTERNAL", YELLOW + BOLD)
245
+ + " - endpoint, plan, or network prevented a live success"
194
246
  )
195
247
  else:
196
248
  failed.append((test.name, output))
197
- print(color("FAIL", RED + BOLD))
249
+ result = color("FAIL", RED + BOLD)
198
250
 
199
- if output.strip():
200
- lines = output.strip().splitlines()
201
- preview = "\n".join(lines[:30])
202
- if len(lines) > 30:
203
- preview += f"\n... ({len(lines) - 30} more lines)"
204
- print(preview)
251
+ print(f"{result}: {test.name}", flush=True)
252
+
253
+ should_print_output = output.strip() and (
254
+ args.verbose == 2 or (args.verbose == 1 and status != "passed")
255
+ )
256
+ if should_print_output:
257
+ print(output_preview(output))
205
258
 
206
259
  print("\n=== Summary ===")
207
260
  print(f"{color('Passed', GREEN)} : {len(passed)}")
261
+ print(f"{color('Auth', ORANGE)} : {len(auth)}")
208
262
  print(f"{color('External', YELLOW)} : {len(external)}")
209
263
  print(f"{color('Failed', RED)} : {len(failed)}")
210
264
 
265
+ if auth:
266
+ print("\nAuth failures:")
267
+ for name in auth:
268
+ print(f" - {name}")
269
+
211
270
  if external:
212
271
  print("\nExternal failures:")
213
272
  for name in external:
@@ -216,8 +275,10 @@ def main() -> int:
216
275
  if failed:
217
276
  print("\nUnexpected failures:")
218
277
  for name, output in failed:
219
- print(f"\n--- {name} ---")
220
- print(output.strip())
278
+ print(f" - {name}")
279
+ if args.verbose >= 1:
280
+ print(f"\n--- {name} ---")
281
+ print(output.strip())
221
282
  return 1
222
283
 
223
284
  return 0
@@ -64,9 +64,20 @@ def fake_ws(monkeypatch):
64
64
  """Patch `connect` so `WebSocketClient.__init__` returns a FakeWebSocket."""
65
65
  fake = FakeWebSocket()
66
66
 
67
- def fake_connect(uri, proxy=None):
67
+ def fake_connect(
68
+ uri,
69
+ proxy=None,
70
+ open_timeout=None,
71
+ ping_interval=None,
72
+ ping_timeout=None,
73
+ close_timeout=None,
74
+ ):
68
75
  fake.uri = uri
69
76
  fake.proxy = proxy
77
+ fake.open_timeout = open_timeout
78
+ fake.ping_interval = ping_interval
79
+ fake.ping_timeout = ping_timeout
80
+ fake.close_timeout = close_timeout
70
81
  return fake
71
82
 
72
83
  monkeypatch.setattr(ws_module, "connect", fake_connect)
@@ -283,13 +294,29 @@ def test_init_picks_up_api_key_from_env(monkeypatch):
283
294
  monkeypatch.setenv("HELIUS_API_KEY", "from-env")
284
295
  monkeypatch.setattr(ws_module, "dotenv_values", lambda: {})
285
296
 
286
- def fake_connect(uri, proxy=None):
297
+ def fake_connect(
298
+ uri,
299
+ proxy=None,
300
+ open_timeout=None,
301
+ ping_interval=None,
302
+ ping_timeout=None,
303
+ close_timeout=None,
304
+ ):
287
305
  fake.uri = uri
306
+ fake.proxy = proxy
307
+ fake.open_timeout = open_timeout
308
+ fake.ping_interval = ping_interval
309
+ fake.ping_timeout = ping_timeout
310
+ fake.close_timeout = close_timeout
288
311
  return fake
289
312
 
290
313
  monkeypatch.setattr(ws_module, "connect", fake_connect)
291
314
  client = WebSocketClient()
292
315
  assert "api-key=from-env" in fake.uri
316
+ assert fake.open_timeout == 10
317
+ assert fake.ping_interval == 20
318
+ assert fake.ping_timeout == 20
319
+ assert fake.close_timeout == 10
293
320
  assert client._websocket is fake
294
321
 
295
322
 
@@ -297,29 +324,67 @@ def test_init_builds_uri_with_api_key_and_passes_proxy(monkeypatch):
297
324
  fake = FakeWebSocket()
298
325
  captured = {}
299
326
 
300
- def fake_connect(uri, proxy=None):
327
+ def fake_connect(
328
+ uri,
329
+ proxy=None,
330
+ open_timeout=None,
331
+ ping_interval=None,
332
+ ping_timeout=None,
333
+ close_timeout=None,
334
+ ):
301
335
  captured["uri"] = uri
302
336
  captured["proxy"] = proxy
337
+ captured["open_timeout"] = open_timeout
338
+ captured["ping_interval"] = ping_interval
339
+ captured["ping_timeout"] = ping_timeout
340
+ captured["close_timeout"] = close_timeout
303
341
  return fake
304
342
 
305
343
  monkeypatch.setattr(ws_module, "connect", fake_connect)
306
- WebSocketClient(api_key="test", proxy="http://proxy:8080")
344
+ WebSocketClient(
345
+ api_key="test",
346
+ proxy="http://proxy:8080",
347
+ open_timeout=1,
348
+ ping_interval=2,
349
+ ping_timeout=3,
350
+ close_timeout=4,
351
+ )
307
352
  assert "api-key=test" in captured["uri"]
308
353
  assert captured["uri"].startswith("wss://mainnet.helius-rpc.com")
309
354
  assert captured["proxy"] == "http://proxy:8080"
355
+ assert captured["open_timeout"] == 1
356
+ assert captured["ping_interval"] == 2
357
+ assert captured["ping_timeout"] == 3
358
+ assert captured["close_timeout"] == 4
310
359
 
311
360
 
312
361
  def test_init_uses_custom_base_url(monkeypatch):
313
362
  fake = FakeWebSocket()
314
363
  captured = {}
315
364
 
316
- def fake_connect(uri, proxy=None):
365
+ def fake_connect(
366
+ uri,
367
+ proxy=None,
368
+ open_timeout=None,
369
+ ping_interval=None,
370
+ ping_timeout=None,
371
+ close_timeout=None,
372
+ ):
317
373
  captured["uri"] = uri
374
+ captured["proxy"] = proxy
375
+ captured["open_timeout"] = open_timeout
376
+ captured["ping_interval"] = ping_interval
377
+ captured["ping_timeout"] = ping_timeout
378
+ captured["close_timeout"] = close_timeout
318
379
  return fake
319
380
 
320
381
  monkeypatch.setattr(ws_module, "connect", fake_connect)
321
382
  WebSocketClient(api_key="test", base_url="wss://devnet.helius-rpc.com")
322
383
  assert captured["uri"].startswith("wss://devnet.helius-rpc.com")
384
+ assert captured["open_timeout"] == 10
385
+ assert captured["ping_interval"] == 20
386
+ assert captured["ping_timeout"] == 20
387
+ assert captured["close_timeout"] == 10
323
388
 
324
389
 
325
390
  def test_close_delegates_to_websocket(client, fake_ws):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes