hyperbrowser 1.3.0__tar.gz → 1.4.0__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 (113) hide show
  1. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/PKG-INFO +1 -1
  2. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/sandbox.py +30 -15
  3. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +4 -0
  4. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +30 -15
  5. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +4 -0
  6. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/sandbox_common.py +64 -1
  7. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/transport/async_transport.py +43 -14
  8. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/transport/sync.py +45 -14
  9. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/pyproject.toml +1 -1
  10. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/LICENSE +0 -0
  11. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/README.md +0 -0
  12. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/__init__.py +0 -0
  13. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/_request.py +0 -0
  14. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/async_client.py +0 -0
  15. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/base.py +0 -0
  16. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
  17. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
  18. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
  19. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
  20. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
  21. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
  22. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
  23. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
  24. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
  25. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
  26. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
  27. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
  28. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
  29. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
  30. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +0 -0
  31. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
  32. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
  33. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
  34. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
  35. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
  36. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
  37. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
  38. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
  39. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
  40. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
  41. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
  42. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sandboxes/image_build.py +0 -0
  43. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sandboxes/shared.py +0 -0
  44. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
  45. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
  46. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
  47. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
  48. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
  49. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
  50. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
  51. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
  52. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
  53. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
  54. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
  55. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
  56. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
  57. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
  58. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +0 -0
  59. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
  60. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
  61. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
  62. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
  63. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
  64. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
  65. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
  66. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
  67. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/client/sync.py +0 -0
  68. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/config.py +0 -0
  69. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/exceptions.py +0 -0
  70. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/__init__.py +0 -0
  71. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/_parsers.py +0 -0
  72. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/agents/browser_use.py +0 -0
  73. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
  74. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/agents/cua.py +0 -0
  75. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
  76. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
  77. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
  78. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/computer_action.py +0 -0
  79. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/consts.py +0 -0
  80. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/crawl.py +0 -0
  81. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/extension.py +0 -0
  82. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/extract.py +0 -0
  83. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/profile.py +0 -0
  84. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/sandbox.py +0 -0
  85. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/scrape.py +0 -0
  86. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/session.py +0 -0
  87. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/team.py +0 -0
  88. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/volume.py +0 -0
  89. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
  90. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/web/branding.py +0 -0
  91. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/web/common.py +0 -0
  92. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/web/crawl.py +0 -0
  93. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/web/fetch.py +0 -0
  94. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/models/web/search.py +0 -0
  95. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/py.typed +0 -0
  96. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/tools/__init__.py +0 -0
  97. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/tools/anthropic.py +0 -0
  98. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/tools/openai.py +0 -0
  99. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/tools/schema.py +0 -0
  100. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/transport/base.py +0 -0
  101. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/__init__.py +0 -0
  102. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/_json.py +0 -0
  103. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/agents.py +0 -0
  104. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/computer_action.py +0 -0
  105. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/crawl.py +0 -0
  106. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/extension.py +0 -0
  107. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/extract.py +0 -0
  108. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/profile.py +0 -0
  109. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/sandbox.py +0 -0
  110. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/scrape.py +0 -0
  111. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/session.py +0 -0
  112. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/volume.py +0 -0
  113. {hyperbrowser-1.3.0 → hyperbrowser-1.4.0}/hyperbrowser/types/web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperbrowser
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Python SDK for hyperbrowser
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -57,8 +57,11 @@ from ....types import (
57
57
  from ....sandbox_common import (
58
58
  RuntimeConnection,
59
59
  ensure_response_ok,
60
+ get_retry_delay_seconds,
60
61
  normalize_network_error,
62
+ request_context,
61
63
  parse_json_response,
64
+ should_retry_get,
62
65
  )
63
66
  from ..sandboxes.shared import (
64
67
  _build_sandbox_exposed_url,
@@ -942,19 +945,31 @@ class SandboxManager:
942
945
  params: Optional[Dict[str, object]] = None,
943
946
  data: Optional[Dict[str, object]] = None,
944
947
  ):
945
- try:
946
- response = await self._client.transport.client.request(
947
- method,
948
- self._client._build_url(path),
949
- params={k: v for k, v in (params or {}).items() if v is not None},
950
- json=data,
951
- )
952
- except BaseException as error:
953
- raise normalize_network_error(
954
- error,
955
- "control",
956
- "Unknown error occurred",
957
- )
948
+ failed_attempt = 1
949
+ while True:
950
+ try:
951
+ response = await self._client.transport.client.request(
952
+ method,
953
+ self._client._build_url(path),
954
+ params={
955
+ key: value
956
+ for key, value in (params or {}).items()
957
+ if value is not None
958
+ },
959
+ json=data,
960
+ )
961
+ ensure_response_ok(response, "control")
962
+ except BaseException as cause:
963
+ error = normalize_network_error(
964
+ cause,
965
+ "control",
966
+ "Unknown error occurred",
967
+ request_context(method, path),
968
+ )
969
+ if not should_retry_get(method, error, failed_attempt):
970
+ raise error
971
+ await asyncio.sleep(get_retry_delay_seconds(failed_attempt))
972
+ failed_attempt += 1
973
+ continue
958
974
 
959
- ensure_response_ok(response, "control")
960
- return parse_json_response(response, "control")
975
+ return parse_json_response(response, "control")
@@ -9,6 +9,7 @@ from .....sandbox_common import (
9
9
  ensure_response_ok,
10
10
  normalize_network_error,
11
11
  parse_json_response,
12
+ request_context,
12
13
  resolve_runtime_transport_target,
13
14
  )
14
15
  from ...sandboxes.shared import _build_query_path, _is_replayable_http_content
@@ -275,6 +276,7 @@ class RuntimeTransport:
275
276
  error,
276
277
  "runtime",
277
278
  "Unknown runtime request error",
279
+ request_context(method, path),
278
280
  )
279
281
 
280
282
  await response.aread()
@@ -309,6 +311,7 @@ class RuntimeTransport:
309
311
  error,
310
312
  "runtime",
311
313
  "Unknown runtime request error",
314
+ request_context(method, path),
312
315
  )
313
316
 
314
317
  async def _send_stream(
@@ -341,4 +344,5 @@ class RuntimeTransport:
341
344
  error,
342
345
  "runtime",
343
346
  "Unknown runtime request error",
347
+ request_context("GET", path),
344
348
  )
@@ -55,8 +55,11 @@ from ....types import (
55
55
  from ....sandbox_common import (
56
56
  RuntimeConnection,
57
57
  ensure_response_ok,
58
+ get_retry_delay_seconds,
58
59
  normalize_network_error,
60
+ request_context,
59
61
  parse_json_response,
62
+ should_retry_get,
60
63
  )
61
64
  from ..sandboxes.shared import (
62
65
  _build_sandbox_exposed_url,
@@ -925,19 +928,31 @@ class SandboxManager:
925
928
  params: Optional[Dict[str, object]] = None,
926
929
  data: Optional[Dict[str, object]] = None,
927
930
  ):
928
- try:
929
- response = self._client.transport.client.request(
930
- method,
931
- self._client._build_url(path),
932
- params={k: v for k, v in (params or {}).items() if v is not None},
933
- json=data,
934
- )
935
- except BaseException as error:
936
- raise normalize_network_error(
937
- error,
938
- "control",
939
- "Unknown error occurred",
940
- )
931
+ failed_attempt = 1
932
+ while True:
933
+ try:
934
+ response = self._client.transport.client.request(
935
+ method,
936
+ self._client._build_url(path),
937
+ params={
938
+ key: value
939
+ for key, value in (params or {}).items()
940
+ if value is not None
941
+ },
942
+ json=data,
943
+ )
944
+ ensure_response_ok(response, "control")
945
+ except BaseException as cause:
946
+ error = normalize_network_error(
947
+ cause,
948
+ "control",
949
+ "Unknown error occurred",
950
+ request_context(method, path),
951
+ )
952
+ if not should_retry_get(method, error, failed_attempt):
953
+ raise error
954
+ time.sleep(get_retry_delay_seconds(failed_attempt))
955
+ failed_attempt += 1
956
+ continue
941
957
 
942
- ensure_response_ok(response, "control")
943
- return parse_json_response(response, "control")
958
+ return parse_json_response(response, "control")
@@ -9,6 +9,7 @@ from .....sandbox_common import (
9
9
  ensure_response_ok,
10
10
  normalize_network_error,
11
11
  parse_json_response,
12
+ request_context,
12
13
  resolve_runtime_transport_target,
13
14
  )
14
15
  from ...sandboxes.shared import _build_query_path, _is_replayable_http_content
@@ -273,6 +274,7 @@ class RuntimeTransport:
273
274
  error,
274
275
  "runtime",
275
276
  "Unknown runtime request error",
277
+ request_context(method, path),
276
278
  )
277
279
 
278
280
  response.read()
@@ -307,6 +309,7 @@ class RuntimeTransport:
307
309
  error,
308
310
  "runtime",
309
311
  "Unknown runtime request error",
312
+ request_context(method, path),
310
313
  )
311
314
 
312
315
  def _send_stream(
@@ -339,4 +342,5 @@ class RuntimeTransport:
339
342
  error,
340
343
  "runtime",
341
344
  "Unknown runtime request error",
345
+ request_context("GET", path),
342
346
  )
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import random
2
3
  from dataclasses import dataclass
3
4
  from typing import Any, Dict, Optional, Tuple
4
5
  from urllib.parse import urljoin, urlsplit, urlunsplit
@@ -8,6 +9,9 @@ import httpx
8
9
  from .exceptions import HyperbrowserError, HyperbrowserService
9
10
 
10
11
  RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
12
+ GET_RETRY_MAX_ATTEMPTS = 3
13
+ GET_RETRY_INITIAL_DELAY_SECONDS = 0.25
14
+ GET_RETRY_MAX_DELAY_SECONDS = 1.0
11
15
  RUNTIME_SESSION_REFRESH_BUFFER_MS = 60_000
12
16
 
13
17
 
@@ -45,6 +49,26 @@ def is_retryable_network_error(error: BaseException) -> bool:
45
49
  )
46
50
 
47
51
 
52
+ def should_retry_get(
53
+ method: str,
54
+ error: HyperbrowserError,
55
+ failed_attempt: int,
56
+ ) -> bool:
57
+ return (
58
+ method.upper() == "GET"
59
+ and error.retryable
60
+ and failed_attempt < GET_RETRY_MAX_ATTEMPTS
61
+ )
62
+
63
+
64
+ def get_retry_delay_seconds(failed_attempt: int) -> float:
65
+ maximum_delay = min(
66
+ GET_RETRY_INITIAL_DELAY_SECONDS * (2 ** (failed_attempt - 1)),
67
+ GET_RETRY_MAX_DELAY_SECONDS,
68
+ )
69
+ return random.uniform(maximum_delay / 2, maximum_delay)
70
+
71
+
48
72
  def parse_error_payload(
49
73
  raw_text: str, fallback_message: str
50
74
  ) -> Tuple[str, Optional[str], Any]:
@@ -230,16 +254,55 @@ def to_websocket_transport_target(
230
254
  )
231
255
 
232
256
 
257
+ def request_context(method: Optional[str], path_or_url: Optional[str]) -> str:
258
+ """Render "[POST /sandbox]" for error messages.
259
+
260
+ Accepts either a bare path or a full URL. The query string is dropped so
261
+ request parameters never reach error text or logs.
262
+ """
263
+ normalized_method = (method or "").strip().upper()
264
+ raw_target = (path_or_url or "").strip()
265
+ normalized_path = urlsplit(raw_target).path or raw_target.split("?", 1)[0]
266
+ if normalized_method and normalized_path:
267
+ return f"[{normalized_method} {normalized_path}]"
268
+ if normalized_path:
269
+ return f"[{normalized_path}]"
270
+ return f"[{normalized_method}]" if normalized_method else ""
271
+
272
+
273
+ def describe_network_error(
274
+ error: BaseException,
275
+ default_message: str,
276
+ context: str = "",
277
+ ) -> str:
278
+ suffix = f" {context}" if context else ""
279
+ detail = str(error).strip()
280
+ if detail:
281
+ return f"{detail}{suffix}"
282
+ # Several httpx transport exceptions are raised with no arguments, so str()
283
+ # is empty and the caller's fallback alone would not say which one failed.
284
+ name = type(error).__name__
285
+ base = f"{default_message} ({name})" if default_message else name
286
+ return f"{base}{suffix}"
287
+
288
+
233
289
  def normalize_network_error(
234
290
  error: BaseException,
235
291
  service: HyperbrowserService,
236
292
  default_message: str,
293
+ context: str = "",
237
294
  ) -> HyperbrowserError:
238
295
  if isinstance(error, HyperbrowserError):
239
296
  return error
297
+ if not isinstance(error, Exception):
298
+ # CancelledError, KeyboardInterrupt and SystemExit are control flow, not
299
+ # transport failures. Callers catch BaseException around their requests,
300
+ # so wrapping these would strand the cancellation and report a request
301
+ # the caller itself abandoned as a network error.
302
+ raise error
240
303
 
241
304
  return HyperbrowserError(
242
- str(error) if str(error) else default_message,
305
+ describe_network_error(error, default_message, context),
243
306
  retryable=is_retryable_network_error(error),
244
307
  service=service,
245
308
  cause=error,
@@ -3,6 +3,15 @@ import httpx
3
3
  from typing import Optional
4
4
 
5
5
  from hyperbrowser.exceptions import HyperbrowserError
6
+ from hyperbrowser.sandbox_common import (
7
+ RETRYABLE_STATUS_CODES,
8
+ get_request_id,
9
+ get_retry_delay_seconds,
10
+ is_retryable_network_error,
11
+ normalize_network_error,
12
+ request_context,
13
+ should_retry_get,
14
+ )
6
15
  from .base import TransportStrategy, APIResponse
7
16
 
8
17
 
@@ -49,6 +58,9 @@ class AsyncTransport(TransportStrategy):
49
58
  status_code=response.status_code,
50
59
  response=response,
51
60
  original_error=e,
61
+ request_id=get_request_id(response),
62
+ retryable=response.status_code in RETRYABLE_STATUS_CODES,
63
+ service="control",
52
64
  )
53
65
  return APIResponse.from_status(response.status_code)
54
66
  except httpx.HTTPStatusError as e:
@@ -62,9 +74,17 @@ class AsyncTransport(TransportStrategy):
62
74
  status_code=response.status_code,
63
75
  response=response,
64
76
  original_error=e,
77
+ request_id=get_request_id(response),
78
+ retryable=response.status_code in RETRYABLE_STATUS_CODES,
79
+ service="control",
65
80
  )
66
81
  except httpx.RequestError as e:
67
- raise HyperbrowserError("Request failed", original_error=e)
82
+ raise HyperbrowserError(
83
+ "Request failed",
84
+ original_error=e,
85
+ retryable=is_retryable_network_error(e),
86
+ service="control",
87
+ )
68
88
 
69
89
  async def post(
70
90
  self,
@@ -82,25 +102,34 @@ class AsyncTransport(TransportStrategy):
82
102
  else:
83
103
  response = await self.client.post(url, json=data, **kwargs)
84
104
  return await self._handle_response(response)
85
- except HyperbrowserError:
86
- raise
87
- except Exception as e:
88
- raise HyperbrowserError("Post request failed", original_error=e)
105
+ except BaseException as e:
106
+ raise normalize_network_error(
107
+ e, "control", "Post request failed", request_context("POST", url)
108
+ )
89
109
 
90
110
  async def get(
91
111
  self, url: str, params: Optional[dict] = None, follow_redirects: bool = False
92
112
  ) -> APIResponse:
93
113
  if params:
94
114
  params = {k: v for k, v in params.items() if v is not None}
95
- try:
96
- response = await self.client.get(
97
- url, params=params, follow_redirects=follow_redirects
98
- )
99
- return await self._handle_response(response)
100
- except HyperbrowserError:
101
- raise
102
- except Exception as e:
103
- raise HyperbrowserError("Get request failed", original_error=e)
115
+ failed_attempt = 1
116
+ while True:
117
+ try:
118
+ response = await self.client.get(
119
+ url, params=params, follow_redirects=follow_redirects
120
+ )
121
+ return await self._handle_response(response)
122
+ except BaseException as cause:
123
+ error = normalize_network_error(
124
+ cause,
125
+ "control",
126
+ "Get request failed",
127
+ request_context("GET", url),
128
+ )
129
+ if not should_retry_get("GET", error, failed_attempt):
130
+ raise error
131
+ await asyncio.sleep(get_retry_delay_seconds(failed_attempt))
132
+ failed_attempt += 1
104
133
 
105
134
  async def put(self, url: str, data: Optional[dict] = None) -> APIResponse:
106
135
  try:
@@ -1,7 +1,18 @@
1
+ import time
2
+
1
3
  import httpx
2
4
  from typing import Optional
3
5
 
4
6
  from hyperbrowser.exceptions import HyperbrowserError
7
+ from hyperbrowser.sandbox_common import (
8
+ RETRYABLE_STATUS_CODES,
9
+ get_request_id,
10
+ get_retry_delay_seconds,
11
+ is_retryable_network_error,
12
+ normalize_network_error,
13
+ request_context,
14
+ should_retry_get,
15
+ )
5
16
  from .base import TransportStrategy, APIResponse
6
17
 
7
18
 
@@ -25,6 +36,9 @@ class SyncTransport(TransportStrategy):
25
36
  status_code=response.status_code,
26
37
  response=response,
27
38
  original_error=e,
39
+ request_id=get_request_id(response),
40
+ retryable=response.status_code in RETRYABLE_STATUS_CODES,
41
+ service="control",
28
42
  )
29
43
  return APIResponse.from_status(response.status_code)
30
44
  except httpx.HTTPStatusError as e:
@@ -38,9 +52,17 @@ class SyncTransport(TransportStrategy):
38
52
  status_code=response.status_code,
39
53
  response=response,
40
54
  original_error=e,
55
+ request_id=get_request_id(response),
56
+ retryable=response.status_code in RETRYABLE_STATUS_CODES,
57
+ service="control",
41
58
  )
42
59
  except httpx.RequestError as e:
43
- raise HyperbrowserError("Request failed", original_error=e)
60
+ raise HyperbrowserError(
61
+ "Request failed",
62
+ original_error=e,
63
+ retryable=is_retryable_network_error(e),
64
+ service="control",
65
+ )
44
66
 
45
67
  def close(self) -> None:
46
68
  self.client.close()
@@ -61,25 +83,34 @@ class SyncTransport(TransportStrategy):
61
83
  else:
62
84
  response = self.client.post(url, json=data, **kwargs)
63
85
  return self._handle_response(response)
64
- except HyperbrowserError:
65
- raise
66
- except Exception as e:
67
- raise HyperbrowserError("Post request failed", original_error=e)
86
+ except BaseException as e:
87
+ raise normalize_network_error(
88
+ e, "control", "Post request failed", request_context("POST", url)
89
+ )
68
90
 
69
91
  def get(
70
92
  self, url: str, params: Optional[dict] = None, follow_redirects: bool = False
71
93
  ) -> APIResponse:
72
94
  if params:
73
95
  params = {k: v for k, v in params.items() if v is not None}
74
- try:
75
- response = self.client.get(
76
- url, params=params, follow_redirects=follow_redirects
77
- )
78
- return self._handle_response(response)
79
- except HyperbrowserError:
80
- raise
81
- except Exception as e:
82
- raise HyperbrowserError("Get request failed", original_error=e)
96
+ failed_attempt = 1
97
+ while True:
98
+ try:
99
+ response = self.client.get(
100
+ url, params=params, follow_redirects=follow_redirects
101
+ )
102
+ return self._handle_response(response)
103
+ except BaseException as cause:
104
+ error = normalize_network_error(
105
+ cause,
106
+ "control",
107
+ "Get request failed",
108
+ request_context("GET", url),
109
+ )
110
+ if not should_retry_get("GET", error, failed_attempt):
111
+ raise error
112
+ time.sleep(get_retry_delay_seconds(failed_attempt))
113
+ failed_attempt += 1
83
114
 
84
115
  def put(self, url: str, data: Optional[dict] = None) -> APIResponse:
85
116
  try:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hyperbrowser"
3
- version = "1.3.0"
3
+ version = "1.4.0"
4
4
  description = "Python SDK for hyperbrowser"
5
5
  authors = ["Nikhil Shahi <nshahi1998@gmail.com>"]
6
6
  license = "MIT"
File without changes
File without changes