helius-python 0.5.0__tar.gz → 0.5.1__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.1}/PKG-INFO +1 -1
  2. {helius_python-0.5.0 → helius_python-0.5.1}/pyproject.toml +1 -1
  3. {helius_python-0.5.0 → helius_python-0.5.1}/test_examples.py +83 -22
  4. {helius_python-0.5.0 → helius_python-0.5.1}/.editorconfig +0 -0
  5. {helius_python-0.5.0 → helius_python-0.5.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  6. {helius_python-0.5.0 → helius_python-0.5.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  7. {helius_python-0.5.0 → helius_python-0.5.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  8. {helius_python-0.5.0 → helius_python-0.5.1}/.github/workflows/python-package.yml +0 -0
  9. {helius_python-0.5.0 → helius_python-0.5.1}/.github/workflows/python-publish.yml +0 -0
  10. {helius_python-0.5.0 → helius_python-0.5.1}/.gitignore +0 -0
  11. {helius_python-0.5.0 → helius_python-0.5.1}/AGENTS.md +0 -0
  12. {helius_python-0.5.0 → helius_python-0.5.1}/CLAUDE.md +0 -0
  13. {helius_python-0.5.0 → helius_python-0.5.1}/CONTRIBUTING.md +0 -0
  14. {helius_python-0.5.0 → helius_python-0.5.1}/LICENSE +0 -0
  15. {helius_python-0.5.0 → helius_python-0.5.1}/README.md +0 -0
  16. {helius_python-0.5.0 → helius_python-0.5.1}/TODO.md +0 -0
  17. {helius_python-0.5.0 → helius_python-0.5.1}/examples/laserstream/websocket_logs.py +0 -0
  18. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.1/examples/rpc}/address_transactions.py +0 -0
  19. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.1/examples/rpc}/address_transfers.py +0 -0
  20. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.1/examples/rpc}/block_explorer.py +0 -0
  21. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.1/examples/rpc}/devnet_airdrop.py +0 -0
  22. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.1/examples/rpc}/network_status.py +0 -0
  23. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.1/examples/rpc}/priority_fees.py +0 -0
  24. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.1/examples/rpc}/stake_overview.py +0 -0
  25. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.1/examples/rpc}/token_inspector.py +0 -0
  26. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.1/examples/rpc}/transaction_inspector.py +0 -0
  27. {helius_python-0.5.0/examples/solana_rpc → helius_python-0.5.1/examples/rpc}/wallet_tracker.py +0 -0
  28. {helius_python-0.5.0 → helius_python-0.5.1}/examples/webhooks/webhook_crud.py +0 -0
  29. {helius_python-0.5.0 → helius_python-0.5.1}/examples/webhooks/webhook_receiver.py +0 -0
  30. {helius_python-0.5.0 → helius_python-0.5.1}/requirements.txt +0 -0
  31. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/__init__.py +0 -0
  32. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/admin/__init__.py +0 -0
  33. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/admin/admin.py +0 -0
  34. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/laserstream/websockets.py +21 -21
  35. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/rpc/__init__.py +0 -0
  36. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/rpc/client.py +0 -0
  37. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/rpc/models.py +0 -0
  38. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/utils/__init__.py +0 -0
  39. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/utils/json_rpc_request.py +0 -0
  40. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/webhooks/__init__.py +0 -0
  41. {helius_python-0.5.0 → helius_python-0.5.1}/src/helius/webhooks/webhooks.py +0 -0
  42. {helius_python-0.5.0 → helius_python-0.5.1}/tests/fixtures/account.json +0 -0
  43. {helius_python-0.5.0 → helius_python-0.5.1}/tests/fixtures/supply.json +0 -0
  44. {helius_python-0.5.0 → helius_python-0.5.1}/tests/unit/admin/test_admin.py +0 -0
  45. {helius_python-0.5.0 → helius_python-0.5.1}/tests/unit/lasterstream/test_websockets.py +0 -0
  46. {helius_python-0.5.0 → helius_python-0.5.1}/tests/unit/solana_rpc/test_client.py +0 -0
  47. {helius_python-0.5.0 → helius_python-0.5.1}/tests/unit/solana_rpc/test_models.py +0 -0
  48. {helius_python-0.5.0 → helius_python-0.5.1}/tests/unit/utils/test_json_rpc_request.py +0 -0
  49. {helius_python-0.5.0 → helius_python-0.5.1}/tests/unit/webhooks/test_webhook.py +0 -0
  50. {helius_python-0.5.0 → helius_python-0.5.1}/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.1
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.1"
14
14
  authors = [
15
15
  { name="Markos Narinian", email="manarinian@gmail.com" },
16
16
  ]
@@ -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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -163,6 +163,27 @@ class WebSocketClient:
163
163
  response = self._websocket.recv()
164
164
  return json.loads(response)
165
165
 
166
+ def receive(self) -> tuple[dict | None, Notification, int]:
167
+ response = json.loads(self._websocket.recv())
168
+ model = self.MODELS[response["method"]]
169
+ result = response["params"]["result"]
170
+ subscription = response["params"]["subscription"]
171
+ if isinstance(result, dict):
172
+ context = result.get("context")
173
+ value = result.get("value")
174
+ else:
175
+ context, value = None, None
176
+ if value is not None:
177
+ notification = model.model_validate(value)
178
+ else:
179
+ notification = model.model_validate(result)
180
+ return context, notification, subscription
181
+
182
+ def listen(self):
183
+ while True:
184
+ context, notification, subscription = self.receive()
185
+ yield context, notification, subscription
186
+
166
187
  def _unsubscribe(self, subscription_type, subscription) -> bool:
167
188
  request = (
168
189
  JsonRpcRequest(method=f"{subscription_type}Unsubscribe")
@@ -376,24 +397,3 @@ class WebSocketClient:
376
397
 
377
398
  def vote_unsubscribe(self, subscription) -> bool:
378
399
  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