hyperbrowser 0.90.1__tar.gz → 0.90.3__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 (91) hide show
  1. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/PKG-INFO +30 -1
  2. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/README.md +29 -0
  3. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/async_client.py +2 -0
  4. hyperbrowser-0.90.3/hyperbrowser/client/managers/async_manager/volume.py +26 -0
  5. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sandboxes/shared.py +23 -6
  6. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +2 -2
  7. hyperbrowser-0.90.3/hyperbrowser/client/managers/sync_manager/volume.py +26 -0
  8. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/sync.py +2 -0
  9. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/__init__.py +9 -0
  10. hyperbrowser-0.90.3/hyperbrowser/models/_parsers.py +11 -0
  11. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/sandbox.py +12 -10
  12. hyperbrowser-0.90.3/hyperbrowser/models/volume.py +29 -0
  13. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/sandbox_common.py +50 -2
  14. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/pyproject.toml +1 -1
  15. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/LICENSE +0 -0
  16. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/__init__.py +0 -0
  17. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/base.py +0 -0
  18. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
  19. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
  20. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
  21. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
  22. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
  23. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
  24. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
  25. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
  26. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
  27. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
  28. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
  29. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/sandbox.py +0 -0
  30. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
  31. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
  32. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +0 -0
  33. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
  34. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
  35. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
  36. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/session.py +0 -0
  37. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/team.py +0 -0
  38. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
  39. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
  40. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
  41. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
  42. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
  43. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
  44. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
  45. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
  46. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
  47. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
  48. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
  49. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
  50. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
  51. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
  52. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
  53. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/sandbox.py +0 -0
  54. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
  55. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +0 -0
  56. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
  57. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
  58. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
  59. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
  60. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
  61. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
  62. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
  63. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
  64. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/config.py +0 -0
  65. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/exceptions.py +0 -0
  66. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/agents/browser_use.py +0 -0
  67. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
  68. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/agents/cua.py +0 -0
  69. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
  70. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/agents/hyper_agent.py +0 -0
  71. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/computer_action.py +0 -0
  72. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/consts.py +0 -0
  73. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/crawl.py +0 -0
  74. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/extension.py +0 -0
  75. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/extract.py +0 -0
  76. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/profile.py +0 -0
  77. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/scrape.py +0 -0
  78. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/session.py +0 -0
  79. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/team.py +0 -0
  80. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/web/batch_fetch.py +0 -0
  81. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/web/common.py +0 -0
  82. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/web/crawl.py +0 -0
  83. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/web/fetch.py +0 -0
  84. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/models/web/search.py +0 -0
  85. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/tools/__init__.py +0 -0
  86. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/tools/anthropic.py +0 -0
  87. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/tools/openai.py +0 -0
  88. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/tools/schema.py +0 -0
  89. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/transport/async_transport.py +0 -0
  90. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/transport/base.py +0 -0
  91. {hyperbrowser-0.90.1 → hyperbrowser-0.90.3}/hyperbrowser/transport/sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperbrowser
3
- Version: 0.90.1
3
+ Version: 0.90.3
4
4
  Summary: Python SDK for hyperbrowser
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -156,6 +156,35 @@ client.close()
156
156
 
157
157
  `cpu`, `memory_mib`, and `disk_mib` are only supported for image launches.
158
158
 
159
+ ### Manage volumes and mount them in a sandbox
160
+
161
+ ```python
162
+ from hyperbrowser import Hyperbrowser
163
+ from hyperbrowser.models import CreateSandboxParams, CreateVolumeParams, SandboxVolumeMount
164
+
165
+ client = Hyperbrowser(api_key="test-key")
166
+
167
+ volume = client.volumes.create(CreateVolumeParams(name="project-cache"))
168
+ all_volumes = client.volumes.list()
169
+ same_volume = client.volumes.get(volume.id)
170
+
171
+ sandbox = client.sandboxes.create(
172
+ CreateSandboxParams(
173
+ image_name="node",
174
+ mounts={
175
+ "/workspace/cache": SandboxVolumeMount(
176
+ id=same_volume.id,
177
+ type="rw",
178
+ shared=True,
179
+ )
180
+ },
181
+ )
182
+ )
183
+
184
+ sandbox.stop()
185
+ client.close()
186
+ ```
187
+
159
188
  ### List sandboxes with filters
160
189
 
161
190
  ```python
@@ -130,6 +130,35 @@ client.close()
130
130
 
131
131
  `cpu`, `memory_mib`, and `disk_mib` are only supported for image launches.
132
132
 
133
+ ### Manage volumes and mount them in a sandbox
134
+
135
+ ```python
136
+ from hyperbrowser import Hyperbrowser
137
+ from hyperbrowser.models import CreateSandboxParams, CreateVolumeParams, SandboxVolumeMount
138
+
139
+ client = Hyperbrowser(api_key="test-key")
140
+
141
+ volume = client.volumes.create(CreateVolumeParams(name="project-cache"))
142
+ all_volumes = client.volumes.list()
143
+ same_volume = client.volumes.get(volume.id)
144
+
145
+ sandbox = client.sandboxes.create(
146
+ CreateSandboxParams(
147
+ image_name="node",
148
+ mounts={
149
+ "/workspace/cache": SandboxVolumeMount(
150
+ id=same_volume.id,
151
+ type="rw",
152
+ shared=True,
153
+ )
154
+ },
155
+ )
156
+ )
157
+
158
+ sandbox.stop()
159
+ client.close()
160
+ ```
161
+
133
162
  ### List sandboxes with filters
134
163
 
135
164
  ```python
@@ -14,6 +14,7 @@ from .managers.async_manager.scrape import ScrapeManager
14
14
  from .managers.async_manager.session import SessionManager
15
15
  from .managers.async_manager.team import TeamManager
16
16
  from .managers.async_manager.computer_action import ComputerActionManager
17
+ from .managers.async_manager.volume import VolumeManager
17
18
 
18
19
 
19
20
  class AsyncHyperbrowser(HyperbrowserBase):
@@ -47,6 +48,7 @@ class AsyncHyperbrowser(HyperbrowserBase):
47
48
  self.team = TeamManager(self)
48
49
  self.computer_action = ComputerActionManager(self)
49
50
  self.sandboxes = SandboxManager(self)
51
+ self.volumes = VolumeManager(self)
50
52
 
51
53
  async def close(self) -> None:
52
54
  await self.transport.close()
@@ -0,0 +1,26 @@
1
+ from hyperbrowser.models.volume import CreateVolumeParams, Volume, VolumeListResponse
2
+
3
+
4
+ class VolumeManager:
5
+ def __init__(self, client):
6
+ self._client = client
7
+
8
+ async def create(self, params: CreateVolumeParams) -> Volume:
9
+ if not isinstance(params, CreateVolumeParams):
10
+ raise TypeError("params must be a CreateVolumeParams instance")
11
+
12
+ response = await self._client.transport.post(
13
+ self._client._build_url("/volume"),
14
+ data=params.model_dump(exclude_none=True, by_alias=True),
15
+ )
16
+ return Volume(**response.data)
17
+
18
+ async def list(self) -> VolumeListResponse:
19
+ response = await self._client.transport.get(self._client._build_url("/volume"))
20
+ return VolumeListResponse(**response.data)
21
+
22
+ async def get(self, volume_id: str) -> Volume:
23
+ response = await self._client.transport.get(
24
+ self._client._build_url(f"/volume/{volume_id}")
25
+ )
26
+ return Volume(**response.data)
@@ -17,6 +17,7 @@ from ....sandbox_common import (
17
17
  RUNTIME_SESSION_REFRESH_BUFFER_MS,
18
18
  normalize_network_error,
19
19
  parse_error_payload,
20
+ runtime_base_url_session_id,
20
21
  )
21
22
 
22
23
  DEFAULT_WATCH_TIMEOUT_MS = 60_000
@@ -84,13 +85,31 @@ def _normalize_exec_params(
84
85
  return _normalize_legacy_process_fields(normalized)
85
86
 
86
87
 
88
+ def _resolve_sandbox_runtime_session_host(runtime, base_url) -> str:
89
+ session_id_from_base_path = runtime_base_url_session_id(base_url.path)
90
+ if session_id_from_base_path and base_url.hostname:
91
+ return f"{session_id_from_base_path}.{base_url.hostname}"
92
+
93
+ runtime_host = str(getattr(runtime, "host", "") or "").strip()
94
+ if runtime_host:
95
+ parsed_host = urlsplit(runtime_host)
96
+ if parsed_host.hostname:
97
+ session_id_from_host_path = runtime_base_url_session_id(parsed_host.path)
98
+ if session_id_from_host_path:
99
+ return f"{session_id_from_host_path}.{parsed_host.hostname}"
100
+ return parsed_host.hostname
101
+ return runtime_host
102
+
103
+ return base_url.hostname or ""
104
+
105
+
87
106
  def _build_sandbox_exposed_url(runtime, port: int) -> str:
88
107
  parsed = urlsplit(runtime.base_url)
89
- hostname = parsed.hostname
90
- if not hostname:
108
+ session_host = _resolve_sandbox_runtime_session_host(runtime, parsed)
109
+ if not session_host:
91
110
  return runtime.base_url
92
111
 
93
- exposed_host = f"{port}-{hostname}"
112
+ exposed_host = f"{port}-{session_host}"
94
113
  netloc = exposed_host
95
114
  if parsed.port:
96
115
  netloc = f"{netloc}:{parsed.port}"
@@ -100,9 +119,7 @@ def _build_sandbox_exposed_url(runtime, port: int) -> str:
100
119
  credentials = f"{credentials}:{parsed.password}"
101
120
  netloc = f"{credentials}@{netloc}"
102
121
 
103
- path = parsed.path or "/"
104
-
105
- return urlunsplit((parsed.scheme, netloc, path, parsed.query, parsed.fragment))
122
+ return urlunsplit((parsed.scheme, netloc, "/", "", ""))
106
123
 
107
124
 
108
125
  def _expires_within_buffer(expires_at: Optional[datetime]) -> bool:
@@ -292,8 +292,8 @@ class SandboxFilesApi:
292
292
  "/sandbox/files",
293
293
  params=self._with_run_as_params(
294
294
  {
295
- "path": path,
296
- "depth": depth,
295
+ "path": path,
296
+ "depth": depth,
297
297
  }
298
298
  ),
299
299
  )
@@ -0,0 +1,26 @@
1
+ from hyperbrowser.models.volume import CreateVolumeParams, Volume, VolumeListResponse
2
+
3
+
4
+ class VolumeManager:
5
+ def __init__(self, client):
6
+ self._client = client
7
+
8
+ def create(self, params: CreateVolumeParams) -> Volume:
9
+ if not isinstance(params, CreateVolumeParams):
10
+ raise TypeError("params must be a CreateVolumeParams instance")
11
+
12
+ response = self._client.transport.post(
13
+ self._client._build_url("/volume"),
14
+ data=params.model_dump(exclude_none=True, by_alias=True),
15
+ )
16
+ return Volume(**response.data)
17
+
18
+ def list(self) -> VolumeListResponse:
19
+ response = self._client.transport.get(self._client._build_url("/volume"))
20
+ return VolumeListResponse(**response.data)
21
+
22
+ def get(self, volume_id: str) -> Volume:
23
+ response = self._client.transport.get(
24
+ self._client._build_url(f"/volume/{volume_id}")
25
+ )
26
+ return Volume(**response.data)
@@ -14,6 +14,7 @@ from .managers.sync_manager.scrape import ScrapeManager
14
14
  from .managers.sync_manager.session import SessionManager
15
15
  from .managers.sync_manager.team import TeamManager
16
16
  from .managers.sync_manager.computer_action import ComputerActionManager
17
+ from .managers.sync_manager.volume import VolumeManager
17
18
 
18
19
 
19
20
  class Hyperbrowser(HyperbrowserBase):
@@ -47,6 +48,7 @@ class Hyperbrowser(HyperbrowserBase):
47
48
  self.team = TeamManager(self)
48
49
  self.computer_action = ComputerActionManager(self)
49
50
  self.sandboxes = SandboxManager(self)
51
+ self.volumes = VolumeManager(self)
50
52
 
51
53
  def close(self) -> None:
52
54
  self.transport.close()
@@ -173,6 +173,7 @@ from .profile import (
173
173
  ProfileListResponse,
174
174
  ProfileResponse,
175
175
  )
176
+ from .volume import CreateVolumeParams, Volume, VolumeListResponse
176
177
  from .scrape import (
177
178
  BatchScrapeJobResponse,
178
179
  BatchScrapeJobStatusResponse,
@@ -245,6 +246,8 @@ from .sandbox import (
245
246
  Sandbox,
246
247
  SandboxDetail,
247
248
  SandboxRuntimeSession,
249
+ SandboxVolumeMountType,
250
+ SandboxVolumeMount,
248
251
  CreateSandboxParams,
249
252
  StartSandboxFromSnapshotParams,
250
253
  SandboxListParams,
@@ -450,6 +453,10 @@ __all__ = [
450
453
  "ProfileListParams",
451
454
  "ProfileListResponse",
452
455
  "ProfileResponse",
456
+ # volume
457
+ "CreateVolumeParams",
458
+ "Volume",
459
+ "VolumeListResponse",
453
460
  # scrape
454
461
  "BatchScrapeJobResponse",
455
462
  "BatchScrapeJobStatusResponse",
@@ -499,6 +506,8 @@ __all__ = [
499
506
  "Sandbox",
500
507
  "SandboxDetail",
501
508
  "SandboxRuntimeSession",
509
+ "SandboxVolumeMountType",
510
+ "SandboxVolumeMount",
502
511
  "CreateSandboxParams",
503
512
  "StartSandboxFromSnapshotParams",
504
513
  "SandboxListParams",
@@ -0,0 +1,11 @@
1
+ from typing import Any
2
+
3
+
4
+ def _parse_optional_int(value: Any):
5
+ if value is None or isinstance(value, int):
6
+ return value
7
+ if isinstance(value, str) and value.strip() == "":
8
+ return None
9
+ if isinstance(value, str):
10
+ return int(value)
11
+ return value
@@ -3,6 +3,7 @@ from typing import Callable, Dict, List, Literal, Optional, Union
3
3
 
4
4
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
5
5
 
6
+ from ._parsers import _parse_optional_int
6
7
  from .session import SessionLaunchState, SessionStatus
7
8
 
8
9
  SandboxStatus = SessionStatus
@@ -29,6 +30,7 @@ SandboxFileEncoding = Literal["utf8", "base64"]
29
30
  SandboxFileReadFormat = Literal["text", "bytes", "blob", "stream"]
30
31
  SandboxFileWatchRoute = Literal["ws", "stream"]
31
32
  SandboxFileSystemEventType = Literal["chmod", "create", "remove", "rename", "write"]
33
+ SandboxVolumeMountType = Literal["rw", "ro"]
32
34
 
33
35
 
34
36
  def _parse_optional_datetime(value):
@@ -37,16 +39,6 @@ def _parse_optional_datetime(value):
37
39
  return value
38
40
 
39
41
 
40
- def _parse_optional_int(value):
41
- if value is None or isinstance(value, int):
42
- return value
43
- if isinstance(value, str) and value.strip() == "":
44
- return None
45
- if isinstance(value, str):
46
- return int(value)
47
- return value
48
-
49
-
50
42
  def _parse_optional_datetime_from_millis(value):
51
43
  if value in (None, ""):
52
44
  return None
@@ -95,6 +87,12 @@ class SandboxUnexposeResult(SandboxBaseModel):
95
87
  exposed: bool
96
88
 
97
89
 
90
+ class SandboxVolumeMount(SandboxBaseModel):
91
+ id: str
92
+ type: Optional[SandboxVolumeMountType] = None
93
+ shared: Optional[bool] = None
94
+
95
+
98
96
  class Sandbox(SandboxBaseModel):
99
97
  id: str
100
98
  team_id: str = Field(alias="teamId")
@@ -180,6 +178,10 @@ class CreateSandboxParams(SandboxBaseModel):
180
178
  default=None,
181
179
  serialization_alias="exposedPorts",
182
180
  )
181
+ mounts: Optional[Dict[str, SandboxVolumeMount]] = Field(
182
+ default=None,
183
+ serialization_alias="mounts",
184
+ )
183
185
  timeout_minutes: Optional[int] = Field(
184
186
  default=None, serialization_alias="timeoutMinutes"
185
187
  )
@@ -0,0 +1,29 @@
1
+ from typing import List, Optional
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
4
+
5
+ from ._parsers import _parse_optional_int
6
+
7
+
8
+ class VolumeBaseModel(BaseModel):
9
+ model_config = ConfigDict(populate_by_name=True)
10
+
11
+
12
+ class CreateVolumeParams(VolumeBaseModel):
13
+ name: str
14
+
15
+
16
+ class Volume(VolumeBaseModel):
17
+ id: str
18
+ name: str
19
+ size: Optional[int] = None
20
+ transfer_amount: Optional[int] = Field(default=None, alias="transferAmount")
21
+
22
+ @field_validator("size", "transfer_amount", mode="before")
23
+ @classmethod
24
+ def parse_optional_int_fields(cls, value):
25
+ return _parse_optional_int(value)
26
+
27
+
28
+ class VolumeListResponse(VolumeBaseModel):
29
+ volumes: List[Volume]
@@ -114,13 +114,61 @@ def has_scheme(value: str) -> bool:
114
114
  return "://" in value
115
115
 
116
116
 
117
+ def runtime_base_url_session_id(runtime_base_url: str) -> Optional[str]:
118
+ parsed_base_url = urlsplit(runtime_base_url.strip())
119
+ segments = [
120
+ segment
121
+ for segment in parsed_base_url.path.strip().strip("/").split("/")
122
+ if segment
123
+ ]
124
+ if len(segments) < 2 or segments[0] != "sandbox" or not segments[1].strip():
125
+ return None
126
+ return segments[1].strip()
127
+
128
+
129
+ def should_prepend_sandbox_to_runtime_api(runtime_base_url: str) -> bool:
130
+ return runtime_base_url_session_id(runtime_base_url) is None
131
+
132
+
133
+ def normalize_runtime_api_path(pathname: str, prepend_sandbox: bool) -> str:
134
+ trimmed = pathname.strip()
135
+ if trimmed == "":
136
+ return "/sandbox" if prepend_sandbox else "/"
137
+
138
+ absolute = trimmed if trimmed.startswith("/") else f"/{trimmed}"
139
+ if prepend_sandbox:
140
+ if absolute == "/sandbox" or absolute.startswith("/sandbox/"):
141
+ return absolute
142
+ return f"/sandbox{absolute}"
143
+
144
+ if absolute == "/sandbox":
145
+ return "/"
146
+ if absolute.startswith("/sandbox/"):
147
+ return f"/{absolute[len('/sandbox/'):]}"
148
+ return absolute
149
+
150
+
151
+ def normalize_runtime_relative_path(base_url: str, path: str) -> str:
152
+ trimmed = path.strip()
153
+ if trimmed == "":
154
+ return ""
155
+
156
+ parsed_path = urlsplit(trimmed)
157
+ prepend_sandbox = should_prepend_sandbox_to_runtime_api(base_url)
158
+ normalized_path = normalize_runtime_api_path(parsed_path.path, prepend_sandbox)
159
+ relative_path = normalized_path.lstrip("/")
160
+ return urlunsplit(
161
+ ("", "", relative_path, parsed_path.query, parsed_path.fragment)
162
+ )
163
+
164
+
117
165
  def resolve_runtime_transport_target(
118
166
  base_url: str,
119
167
  path: str,
120
168
  runtime_proxy_override: Optional[str] = None,
121
169
  ) -> RuntimeTransportTarget:
122
170
  normalized_base = base_url if base_url.endswith("/") else f"{base_url}/"
123
- url = urljoin(normalized_base, path.lstrip("/"))
171
+ url = urljoin(normalized_base, normalize_runtime_relative_path(base_url, path))
124
172
 
125
173
  if not runtime_proxy_override:
126
174
  return RuntimeTransportTarget(url=url)
@@ -151,7 +199,7 @@ def to_websocket_transport_target(
151
199
  runtime_proxy_override: Optional[str] = None,
152
200
  ) -> RuntimeTransportTarget:
153
201
  normalized_base = base_url if base_url.endswith("/") else f"{base_url}/"
154
- url = urljoin(normalized_base, path.lstrip("/"))
202
+ url = urljoin(normalized_base, normalize_runtime_relative_path(base_url, path))
155
203
  parts = urlsplit(url)
156
204
  scheme = parts.scheme
157
205
  if scheme == "https":
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hyperbrowser"
3
- version = "0.90.1"
3
+ version = "0.90.3"
4
4
  description = "Python SDK for hyperbrowser"
5
5
  authors = ["Nikhil Shahi <nshahi1998@gmail.com>"]
6
6
  license = "MIT"
File without changes