hyperbrowser 0.88.2__tar.gz → 0.89.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 (88) hide show
  1. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/PKG-INFO +113 -1
  2. hyperbrowser-0.89.0/README.md +217 -0
  3. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/sandbox.py +36 -5
  4. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +2 -6
  5. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +12 -2
  6. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sandboxes/shared.py +31 -4
  7. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +36 -5
  8. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +2 -6
  9. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +9 -2
  10. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/__init__.py +1 -0
  11. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/sandbox.py +41 -11
  12. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/pyproject.toml +1 -1
  13. hyperbrowser-0.88.2/README.md +0 -105
  14. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/LICENSE +0 -0
  15. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/__init__.py +0 -0
  16. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/async_client.py +0 -0
  17. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/base.py +0 -0
  18. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
  19. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
  20. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
  21. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
  22. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
  23. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
  24. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
  25. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
  26. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
  27. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
  28. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
  29. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
  30. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +0 -0
  31. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
  32. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
  33. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
  34. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
  35. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
  36. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
  37. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
  38. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
  39. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
  40. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
  41. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
  42. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
  43. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
  44. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
  45. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
  46. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
  47. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
  48. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
  49. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
  50. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
  51. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +0 -0
  52. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
  53. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
  54. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
  55. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
  56. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
  57. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
  58. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
  59. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/client/sync.py +0 -0
  60. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/config.py +0 -0
  61. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/exceptions.py +0 -0
  62. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/agents/browser_use.py +0 -0
  63. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
  64. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/agents/cua.py +0 -0
  65. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
  66. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
  67. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/computer_action.py +0 -0
  68. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/consts.py +0 -0
  69. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/crawl.py +0 -0
  70. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/extension.py +0 -0
  71. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/extract.py +0 -0
  72. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/profile.py +0 -0
  73. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/scrape.py +0 -0
  74. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/session.py +0 -0
  75. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/team.py +0 -0
  76. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
  77. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/web/common.py +0 -0
  78. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/web/crawl.py +0 -0
  79. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/web/fetch.py +0 -0
  80. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/models/web/search.py +0 -0
  81. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/sandbox_common.py +0 -0
  82. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/tools/__init__.py +0 -0
  83. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/tools/anthropic.py +0 -0
  84. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/tools/openai.py +0 -0
  85. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/tools/schema.py +0 -0
  86. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/transport/async_transport.py +0 -0
  87. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/transport/base.py +0 -0
  88. {hyperbrowser-0.88.2 → hyperbrowser-0.89.0}/hyperbrowser/transport/sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperbrowser
3
- Version: 0.88.2
3
+ Version: 0.89.0
4
4
  Summary: Python SDK for hyperbrowser
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -126,6 +126,118 @@ def main():
126
126
  # Run the asyncio event loop
127
127
  main()
128
128
  ```
129
+
130
+ ## Sandboxes
131
+
132
+ The sync and async clients expose the same sandbox APIs through `client.sandboxes`.
133
+
134
+ ### Create a sandbox with pre-exposed ports
135
+
136
+ ```python
137
+ from hyperbrowser import Hyperbrowser
138
+ from hyperbrowser.models import CreateSandboxParams, SandboxExposeParams
139
+
140
+ client = Hyperbrowser(api_key="test-key")
141
+ sandbox = client.sandboxes.create(
142
+ CreateSandboxParams(
143
+ image_name="node",
144
+ exposed_ports=[SandboxExposeParams(port=3000, auth=True)],
145
+ )
146
+ )
147
+
148
+ print(sandbox.exposed_ports[0].browser_url)
149
+ sandbox.stop()
150
+ client.close()
151
+ ```
152
+
153
+ ### List sandboxes with filters
154
+
155
+ ```python
156
+ from hyperbrowser import Hyperbrowser
157
+ from hyperbrowser.models import SandboxListParams
158
+
159
+ client = Hyperbrowser(api_key="test-key")
160
+ result = client.sandboxes.list(
161
+ SandboxListParams(
162
+ status="active",
163
+ search="sandbox",
164
+ start=1711929600000,
165
+ end=1712016000000,
166
+ limit=20,
167
+ )
168
+ )
169
+
170
+ for sandbox in result.sandboxes:
171
+ print(sandbox.id, sandbox.status)
172
+ ```
173
+
174
+ ### List snapshots for a specific image
175
+
176
+ ```python
177
+ from hyperbrowser import Hyperbrowser
178
+ from hyperbrowser.models import SandboxSnapshotListParams
179
+
180
+ client = Hyperbrowser(api_key="test-key")
181
+ snapshots = client.sandboxes.list_snapshots(
182
+ SandboxSnapshotListParams(image_name="node", status="created", limit=10)
183
+ )
184
+ ```
185
+
186
+ ### Expose and unexpose ports
187
+
188
+ ```python
189
+ from hyperbrowser import Hyperbrowser
190
+ from hyperbrowser.models import CreateSandboxParams, SandboxExposeParams
191
+
192
+ client = Hyperbrowser(api_key="test-key")
193
+ sandbox = client.sandboxes.create(CreateSandboxParams(image_name="node"))
194
+
195
+ result = sandbox.expose(SandboxExposeParams(port=8080, auth=True))
196
+ print(result.url, result.browser_url)
197
+
198
+ sandbox.unexpose(8080)
199
+ ```
200
+
201
+ ### Batch file writes with per-file options
202
+
203
+ ```python
204
+ from hyperbrowser import Hyperbrowser
205
+ from hyperbrowser.models import CreateSandboxParams, SandboxFileWriteEntry
206
+
207
+ client = Hyperbrowser(api_key="test-key")
208
+ sandbox = client.sandboxes.create(CreateSandboxParams(image_name="node"))
209
+
210
+ sandbox.files.write(
211
+ [
212
+ SandboxFileWriteEntry(
213
+ path="/tmp/config.json",
214
+ data='{"debug":true}\n',
215
+ append=True,
216
+ mode="600",
217
+ ),
218
+ SandboxFileWriteEntry(
219
+ path="/tmp/blob.bin",
220
+ data=b"\x00\x01\x02",
221
+ ),
222
+ ]
223
+ )
224
+ ```
225
+
226
+ ### Resume terminal output after reconnect
227
+
228
+ ```python
229
+ from hyperbrowser import Hyperbrowser
230
+ from hyperbrowser.models import CreateSandboxParams, SandboxTerminalCreateParams
231
+
232
+ client = Hyperbrowser(api_key="test-key")
233
+ sandbox = client.sandboxes.create(CreateSandboxParams(image_name="node"))
234
+ terminal = sandbox.terminal.create(SandboxTerminalCreateParams(command="bash"))
235
+
236
+ connection = terminal.attach(cursor=10)
237
+ for event in connection.events():
238
+ print(event)
239
+ ```
240
+
129
241
  ## License
130
242
 
131
243
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,217 @@
1
+ # Hyperbrowser Python SDK
2
+
3
+ Checkout the full documentation [here](https://hyperbrowser.ai/docs)
4
+
5
+ ## Installation
6
+
7
+ Currently Hyperbrowser supports creating a browser session in two ways:
8
+
9
+ - Async Client
10
+ - Sync Client
11
+
12
+ It can be installed from `pypi` by running :
13
+
14
+ ```shell
15
+ pip install hyperbrowser
16
+ ```
17
+
18
+ ## Configuration
19
+
20
+ Both the sync and async client follow similar configuration params
21
+
22
+ ### API Key
23
+ The API key can be configured either from the constructor arguments or environment variables using `HYPERBROWSER_API_KEY`
24
+
25
+ ## Usage
26
+
27
+ ### Async
28
+
29
+ ```python
30
+ import asyncio
31
+ from pyppeteer import connect
32
+ from hyperbrowser import AsyncHyperbrowser
33
+
34
+ HYPERBROWSER_API_KEY = "test-key"
35
+
36
+ async def main():
37
+ async with AsyncHyperbrowser(api_key=HYPERBROWSER_API_KEY) as client:
38
+ session = await client.sessions.create()
39
+
40
+ ws_endpoint = session.ws_endpoint
41
+ browser = await connect(browserWSEndpoint=ws_endpoint, defaultViewport=None)
42
+
43
+ # Get pages
44
+ pages = await browser.pages()
45
+ if not pages:
46
+ raise Exception("No pages available")
47
+
48
+ page = pages[0]
49
+
50
+ # Navigate to a website
51
+ print("Navigating to Hacker News...")
52
+ await page.goto("https://news.ycombinator.com/")
53
+ page_title = await page.title()
54
+ print("Page title:", page_title)
55
+
56
+ await page.close()
57
+ await browser.disconnect()
58
+ await client.sessions.stop(session.id)
59
+ print("Session completed!")
60
+
61
+ # Run the asyncio event loop
62
+ asyncio.get_event_loop().run_until_complete(main())
63
+ ```
64
+ ### Sync
65
+
66
+ ```python
67
+ from playwright.sync_api import sync_playwright
68
+ from hyperbrowser import Hyperbrowser
69
+
70
+ HYPERBROWSER_API_KEY = "test-key"
71
+
72
+ def main():
73
+ client = Hyperbrowser(api_key=HYPERBROWSER_API_KEY)
74
+ session = client.sessions.create()
75
+
76
+ ws_endpoint = session.ws_endpoint
77
+
78
+ # Launch Playwright and connect to the remote browser
79
+ with sync_playwright() as p:
80
+ browser = p.chromium.connect_over_cdp(ws_endpoint)
81
+ context = browser.new_context()
82
+
83
+ # Get the first page or create a new one
84
+ if len(context.pages) == 0:
85
+ page = context.new_page()
86
+ else:
87
+ page = context.pages[0]
88
+
89
+ # Navigate to a website
90
+ print("Navigating to Hacker News...")
91
+ page.goto("https://news.ycombinator.com/")
92
+ page_title = page.title()
93
+ print("Page title:", page_title)
94
+
95
+ page.close()
96
+ browser.close()
97
+ print("Session completed!")
98
+ client.sessions.stop(session.id)
99
+
100
+ # Run the asyncio event loop
101
+ main()
102
+ ```
103
+
104
+ ## Sandboxes
105
+
106
+ The sync and async clients expose the same sandbox APIs through `client.sandboxes`.
107
+
108
+ ### Create a sandbox with pre-exposed ports
109
+
110
+ ```python
111
+ from hyperbrowser import Hyperbrowser
112
+ from hyperbrowser.models import CreateSandboxParams, SandboxExposeParams
113
+
114
+ client = Hyperbrowser(api_key="test-key")
115
+ sandbox = client.sandboxes.create(
116
+ CreateSandboxParams(
117
+ image_name="node",
118
+ exposed_ports=[SandboxExposeParams(port=3000, auth=True)],
119
+ )
120
+ )
121
+
122
+ print(sandbox.exposed_ports[0].browser_url)
123
+ sandbox.stop()
124
+ client.close()
125
+ ```
126
+
127
+ ### List sandboxes with filters
128
+
129
+ ```python
130
+ from hyperbrowser import Hyperbrowser
131
+ from hyperbrowser.models import SandboxListParams
132
+
133
+ client = Hyperbrowser(api_key="test-key")
134
+ result = client.sandboxes.list(
135
+ SandboxListParams(
136
+ status="active",
137
+ search="sandbox",
138
+ start=1711929600000,
139
+ end=1712016000000,
140
+ limit=20,
141
+ )
142
+ )
143
+
144
+ for sandbox in result.sandboxes:
145
+ print(sandbox.id, sandbox.status)
146
+ ```
147
+
148
+ ### List snapshots for a specific image
149
+
150
+ ```python
151
+ from hyperbrowser import Hyperbrowser
152
+ from hyperbrowser.models import SandboxSnapshotListParams
153
+
154
+ client = Hyperbrowser(api_key="test-key")
155
+ snapshots = client.sandboxes.list_snapshots(
156
+ SandboxSnapshotListParams(image_name="node", status="created", limit=10)
157
+ )
158
+ ```
159
+
160
+ ### Expose and unexpose ports
161
+
162
+ ```python
163
+ from hyperbrowser import Hyperbrowser
164
+ from hyperbrowser.models import CreateSandboxParams, SandboxExposeParams
165
+
166
+ client = Hyperbrowser(api_key="test-key")
167
+ sandbox = client.sandboxes.create(CreateSandboxParams(image_name="node"))
168
+
169
+ result = sandbox.expose(SandboxExposeParams(port=8080, auth=True))
170
+ print(result.url, result.browser_url)
171
+
172
+ sandbox.unexpose(8080)
173
+ ```
174
+
175
+ ### Batch file writes with per-file options
176
+
177
+ ```python
178
+ from hyperbrowser import Hyperbrowser
179
+ from hyperbrowser.models import CreateSandboxParams, SandboxFileWriteEntry
180
+
181
+ client = Hyperbrowser(api_key="test-key")
182
+ sandbox = client.sandboxes.create(CreateSandboxParams(image_name="node"))
183
+
184
+ sandbox.files.write(
185
+ [
186
+ SandboxFileWriteEntry(
187
+ path="/tmp/config.json",
188
+ data='{"debug":true}\n',
189
+ append=True,
190
+ mode="600",
191
+ ),
192
+ SandboxFileWriteEntry(
193
+ path="/tmp/blob.bin",
194
+ data=b"\x00\x01\x02",
195
+ ),
196
+ ]
197
+ )
198
+ ```
199
+
200
+ ### Resume terminal output after reconnect
201
+
202
+ ```python
203
+ from hyperbrowser import Hyperbrowser
204
+ from hyperbrowser.models import CreateSandboxParams, SandboxTerminalCreateParams
205
+
206
+ client = Hyperbrowser(api_key="test-key")
207
+ sandbox = client.sandboxes.create(CreateSandboxParams(image_name="node"))
208
+ terminal = sandbox.terminal.create(SandboxTerminalCreateParams(command="bash"))
209
+
210
+ connection = terminal.attach(cursor=10)
211
+ for event in connection.events():
212
+ print(event)
213
+ ```
214
+
215
+ ## License
216
+
217
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -15,6 +15,7 @@ from ....models.sandbox import (
15
15
  SandboxRuntimeSession,
16
16
  SandboxSnapshotListParams,
17
17
  SandboxSnapshotListResponse,
18
+ SandboxUnexposeResult,
18
19
  StartSandboxFromSnapshotParams,
19
20
  )
20
21
  from ....models.session import BasicResponse
@@ -113,6 +114,10 @@ class SandboxHandle:
113
114
  def session_url(self) -> str:
114
115
  return self._detail.session_url
115
116
 
117
+ @property
118
+ def exposed_ports(self):
119
+ return self._detail.exposed_ports
120
+
116
121
  def to_dict(self):
117
122
  return self._detail.model_dump()
118
123
 
@@ -152,7 +157,27 @@ class SandboxHandle:
152
157
  async def expose(self, params: SandboxExposeParams) -> SandboxExposeResult:
153
158
  if not isinstance(params, SandboxExposeParams):
154
159
  raise TypeError("params must be a SandboxExposeParams instance")
155
- return await self._service.expose(self.id, params, runtime=self.runtime)
160
+ result = await self._service.expose(self.id, params, runtime=self.runtime)
161
+ exposed_ports = [
162
+ port for port in self._detail.exposed_ports if port.port != result.port
163
+ ]
164
+ exposed_ports.append(result)
165
+ exposed_ports.sort(key=lambda port: port.port)
166
+ self._detail = self._detail.model_copy(update={"exposed_ports": exposed_ports})
167
+ return result
168
+
169
+ async def unexpose(self, port: int) -> SandboxUnexposeResult:
170
+ result = await self._service.unexpose(self.id, port)
171
+ self._detail = self._detail.model_copy(
172
+ update={
173
+ "exposed_ports": [
174
+ exposed_port
175
+ for exposed_port in self._detail.exposed_ports
176
+ if exposed_port.port != port
177
+ ]
178
+ }
179
+ )
180
+ return result
156
181
 
157
182
  def get_exposed_url(self, port: int) -> str:
158
183
  return _build_sandbox_exposed_url(self.runtime, port)
@@ -372,11 +397,17 @@ class SandboxManager:
372
397
  data=params.model_dump(exclude_none=True, by_alias=True),
373
398
  )
374
399
  target_runtime = runtime or (await self.get_detail(sandbox_id)).runtime
375
- return SandboxExposeResult(
376
- port=payload["port"],
377
- auth=payload["auth"],
378
- url=_build_sandbox_exposed_url(target_runtime, payload["port"]),
400
+ if "url" not in payload:
401
+ payload["url"] = _build_sandbox_exposed_url(target_runtime, payload["port"])
402
+ return SandboxExposeResult(**payload)
403
+
404
+ async def unexpose(self, sandbox_id: str, port: int) -> SandboxUnexposeResult:
405
+ payload = await self._request(
406
+ "POST",
407
+ f"/sandbox/{sandbox_id}/unexpose",
408
+ data={"port": port},
379
409
  )
410
+ return SandboxUnexposeResult(**payload)
380
411
 
381
412
  async def _create_detail(self, params: CreateSandboxParams) -> SandboxDetail:
382
413
  payload = await self._request(
@@ -32,6 +32,7 @@ from .....models.sandbox import (
32
32
  from .....sandbox_common import build_headers, to_websocket_transport_target
33
33
  from ...sandboxes.shared import (
34
34
  DEFAULT_WATCH_TIMEOUT_MS,
35
+ _encode_batch_write_entry,
35
36
  _copy_model,
36
37
  _encode_write_data,
37
38
  _normalize_event_type,
@@ -371,12 +372,7 @@ class SandboxFilesApi:
371
372
  for entry in path_or_files:
372
373
  if not isinstance(entry, SandboxFileWriteEntry):
373
374
  raise TypeError("files must contain SandboxFileWriteEntry instances")
374
- encoded_files.append(
375
- {
376
- "path": entry.path,
377
- **_encode_write_data(entry.data),
378
- }
379
- )
375
+ encoded_files.append(_encode_batch_write_entry(entry))
380
376
 
381
377
  payload = await self._transport.request_json(
382
378
  "/sandbox/files/write",
@@ -2,6 +2,7 @@ import base64
2
2
  import json
3
3
  import socket
4
4
  from typing import AsyncIterator, Dict, Optional, Union
5
+ from urllib.parse import urlencode
5
6
 
6
7
  from websockets.asyncio.client import connect as async_ws_connect
7
8
  from websockets.exceptions import ConnectionClosed
@@ -160,11 +161,20 @@ class SandboxTerminalHandle:
160
161
  self._status = _normalize_terminal_status(payload["pty"])
161
162
  return self.current
162
163
 
163
- async def attach(self) -> SandboxTerminalConnection:
164
+ async def attach(
165
+ self,
166
+ cursor: Optional[int] = None,
167
+ ) -> SandboxTerminalConnection:
164
168
  connection = await self._get_connection_info()
169
+ query = urlencode(
170
+ [
171
+ ("sessionId", connection.sandbox_id),
172
+ *([("cursor", str(cursor))] if cursor is not None else []),
173
+ ]
174
+ )
165
175
  target = to_websocket_transport_target(
166
176
  connection.base_url,
167
- f"/sandbox/pty/{self.id}/ws?sessionId={connection.sandbox_id}",
177
+ f"/sandbox/pty/{self.id}/ws?{query}",
168
178
  self._runtime_proxy_override,
169
179
  )
170
180
  headers = build_headers(connection.token, host_header=target.host_header)
@@ -7,6 +7,7 @@ from urllib.parse import urlencode, urlsplit, urlunsplit
7
7
  from ....exceptions import HyperbrowserError
8
8
  from ....models.sandbox import (
9
9
  SandboxFileInfo,
10
+ SandboxFileWriteEntry,
10
11
  SandboxFileWriteInfo,
11
12
  SandboxTerminalStatus,
12
13
  )
@@ -27,7 +28,7 @@ def _build_sandbox_exposed_url(runtime, port: int) -> str:
27
28
  parsed = urlsplit(runtime.base_url)
28
29
  hostname = parsed.hostname
29
30
  if not hostname:
30
- return runtime.base_url.rstrip("/")
31
+ return runtime.base_url
31
32
 
32
33
  exposed_host = f"{port}-{hostname}"
33
34
  netloc = exposed_host
@@ -39,9 +40,9 @@ def _build_sandbox_exposed_url(runtime, port: int) -> str:
39
40
  credentials = f"{credentials}:{parsed.password}"
40
41
  netloc = f"{credentials}@{netloc}"
41
42
 
42
- return urlunsplit(
43
- (parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment)
44
- ).rstrip("/")
43
+ path = parsed.path or "/"
44
+
45
+ return urlunsplit((parsed.scheme, netloc, path, parsed.query, parsed.fragment))
45
46
 
46
47
 
47
48
  def _expires_within_buffer(expires_at: Optional[datetime]) -> bool:
@@ -188,6 +189,32 @@ def _encode_write_data(data: Union[str, bytes, bytearray]) -> Dict[str, str]:
188
189
  }
189
190
 
190
191
 
192
+ def _encode_batch_write_entry(entry: SandboxFileWriteEntry) -> Dict[str, object]:
193
+ if isinstance(entry.data, str):
194
+ encoding = entry.encoding or "utf8"
195
+ if encoding not in {"utf8", "base64"}:
196
+ raise ValueError("encoding should be one of: utf8, base64")
197
+ payload: Dict[str, object] = {
198
+ "path": entry.path,
199
+ "data": entry.data,
200
+ "encoding": encoding,
201
+ }
202
+ else:
203
+ if entry.encoding not in {None, "base64"}:
204
+ raise ValueError("encoding must be base64 when data is bytes")
205
+ payload = {
206
+ "path": entry.path,
207
+ "data": base64.b64encode(bytes(entry.data)).decode("ascii"),
208
+ "encoding": "base64",
209
+ }
210
+
211
+ if entry.append is not None:
212
+ payload["append"] = entry.append
213
+ if entry.mode is not None:
214
+ payload["mode"] = entry.mode
215
+ return payload
216
+
217
+
191
218
  def _normalize_terminal_output_chunk(entry: Dict[str, object]) -> Dict[str, object]:
192
219
  raw = base64.b64decode(entry["data"])
193
220
  return {
@@ -15,6 +15,7 @@ from ....models.sandbox import (
15
15
  SandboxRuntimeSession,
16
16
  SandboxSnapshotListParams,
17
17
  SandboxSnapshotListResponse,
18
+ SandboxUnexposeResult,
18
19
  StartSandboxFromSnapshotParams,
19
20
  )
20
21
  from ....models.session import BasicResponse
@@ -113,6 +114,10 @@ class SandboxHandle:
113
114
  def session_url(self) -> str:
114
115
  return self._detail.session_url
115
116
 
117
+ @property
118
+ def exposed_ports(self):
119
+ return self._detail.exposed_ports
120
+
116
121
  def to_dict(self):
117
122
  return self._detail.model_dump()
118
123
 
@@ -152,7 +157,27 @@ class SandboxHandle:
152
157
  def expose(self, params: SandboxExposeParams) -> SandboxExposeResult:
153
158
  if not isinstance(params, SandboxExposeParams):
154
159
  raise TypeError("params must be a SandboxExposeParams instance")
155
- return self._service.expose(self.id, params, runtime=self.runtime)
160
+ result = self._service.expose(self.id, params, runtime=self.runtime)
161
+ exposed_ports = [
162
+ port for port in self._detail.exposed_ports if port.port != result.port
163
+ ]
164
+ exposed_ports.append(result)
165
+ exposed_ports.sort(key=lambda port: port.port)
166
+ self._detail = self._detail.model_copy(update={"exposed_ports": exposed_ports})
167
+ return result
168
+
169
+ def unexpose(self, port: int) -> SandboxUnexposeResult:
170
+ result = self._service.unexpose(self.id, port)
171
+ self._detail = self._detail.model_copy(
172
+ update={
173
+ "exposed_ports": [
174
+ exposed_port
175
+ for exposed_port in self._detail.exposed_ports
176
+ if exposed_port.port != port
177
+ ]
178
+ }
179
+ )
180
+ return result
156
181
 
157
182
  def get_exposed_url(self, port: int) -> str:
158
183
  return _build_sandbox_exposed_url(self.runtime, port)
@@ -372,11 +397,17 @@ class SandboxManager:
372
397
  data=params.model_dump(exclude_none=True, by_alias=True),
373
398
  )
374
399
  target_runtime = runtime or self.get_detail(sandbox_id).runtime
375
- return SandboxExposeResult(
376
- port=payload["port"],
377
- auth=payload["auth"],
378
- url=_build_sandbox_exposed_url(target_runtime, payload["port"]),
400
+ if "url" not in payload:
401
+ payload["url"] = _build_sandbox_exposed_url(target_runtime, payload["port"])
402
+ return SandboxExposeResult(**payload)
403
+
404
+ def unexpose(self, sandbox_id: str, port: int) -> SandboxUnexposeResult:
405
+ payload = self._request(
406
+ "POST",
407
+ f"/sandbox/{sandbox_id}/unexpose",
408
+ data={"port": port},
379
409
  )
410
+ return SandboxUnexposeResult(**payload)
380
411
 
381
412
  def _create_detail(self, params: CreateSandboxParams) -> SandboxDetail:
382
413
  payload = self._request(
@@ -31,6 +31,7 @@ from .....models.sandbox import (
31
31
  from .....sandbox_common import build_headers, to_websocket_transport_target
32
32
  from ...sandboxes.shared import (
33
33
  DEFAULT_WATCH_TIMEOUT_MS,
34
+ _encode_batch_write_entry,
34
35
  _copy_model,
35
36
  _encode_write_data,
36
37
  _normalize_event_type,
@@ -352,12 +353,7 @@ class SandboxFilesApi:
352
353
  for entry in path_or_files:
353
354
  if not isinstance(entry, SandboxFileWriteEntry):
354
355
  raise TypeError("files must contain SandboxFileWriteEntry instances")
355
- encoded_files.append(
356
- {
357
- "path": entry.path,
358
- **_encode_write_data(entry.data),
359
- }
360
- )
356
+ encoded_files.append(_encode_batch_write_entry(entry))
361
357
 
362
358
  payload = self._transport.request_json(
363
359
  "/sandbox/files/write",
@@ -2,6 +2,7 @@ import base64
2
2
  import json
3
3
  import socket
4
4
  from typing import Dict, Optional, Union
5
+ from urllib.parse import urlencode
5
6
 
6
7
  from websockets.exceptions import ConnectionClosed
7
8
  from websockets.sync.client import connect as sync_ws_connect
@@ -160,11 +161,17 @@ class SandboxTerminalHandle:
160
161
  self._status = _normalize_terminal_status(payload["pty"])
161
162
  return self.current
162
163
 
163
- def attach(self) -> SandboxTerminalConnection:
164
+ def attach(self, cursor: Optional[int] = None) -> SandboxTerminalConnection:
164
165
  connection = self._get_connection_info()
166
+ query = urlencode(
167
+ [
168
+ ("sessionId", connection.sandbox_id),
169
+ *([("cursor", str(cursor))] if cursor is not None else []),
170
+ ]
171
+ )
165
172
  target = to_websocket_transport_target(
166
173
  connection.base_url,
167
- f"/sandbox/pty/{self.id}/ws?sessionId={connection.sandbox_id}",
174
+ f"/sandbox/pty/{self.id}/ws?{query}",
168
175
  self._runtime_proxy_override,
169
176
  )
170
177
  headers = build_headers(connection.token, host_header=target.host_header)
@@ -259,6 +259,7 @@ from .sandbox import (
259
259
  SandboxMemorySnapshotResult,
260
260
  SandboxExposeParams,
261
261
  SandboxExposeResult,
262
+ SandboxUnexposeResult,
262
263
  SandboxProcessStatus,
263
264
  SandboxExecParams,
264
265
  SandboxProcessSummary,
@@ -69,6 +69,32 @@ class SandboxRuntimeTarget(SandboxBaseModel):
69
69
  base_url: str = Field(alias="baseUrl")
70
70
 
71
71
 
72
+ class SandboxExposeParams(SandboxBaseModel):
73
+ port: int
74
+ auth: Optional[bool] = None
75
+
76
+
77
+ class SandboxExposeResult(SandboxBaseModel):
78
+ port: int
79
+ auth: bool
80
+ url: str
81
+ browser_url: Optional[str] = Field(default=None, alias="browserUrl")
82
+ browser_url_expires_at: Optional[datetime] = Field(
83
+ default=None,
84
+ alias="browserUrlExpiresAt",
85
+ )
86
+
87
+ @field_validator("browser_url_expires_at", mode="before")
88
+ @classmethod
89
+ def parse_browser_url_expires_at(cls, value):
90
+ return _parse_optional_datetime(value)
91
+
92
+
93
+ class SandboxUnexposeResult(SandboxBaseModel):
94
+ port: int
95
+ exposed: bool
96
+
97
+
72
98
  class Sandbox(SandboxBaseModel):
73
99
  id: str
74
100
  team_id: str = Field(alias="teamId")
@@ -91,6 +117,10 @@ class Sandbox(SandboxBaseModel):
91
117
  duration: int
92
118
  proxy_bytes_used: Optional[int] = Field(default=None, alias="proxyBytesUsed")
93
119
  runtime: SandboxRuntimeTarget
120
+ exposed_ports: List[SandboxExposeResult] = Field(
121
+ default_factory=list,
122
+ alias="exposedPorts",
123
+ )
94
124
 
95
125
  @field_validator(
96
126
  "end_time",
@@ -140,6 +170,10 @@ class CreateSandboxParams(SandboxBaseModel):
140
170
  enable_recording: Optional[bool] = Field(
141
171
  default=None, serialization_alias="enableRecording"
142
172
  )
173
+ exposed_ports: Optional[List[SandboxExposeParams]] = Field(
174
+ default=None,
175
+ serialization_alias="exposedPorts",
176
+ )
143
177
  timeout_minutes: Optional[int] = Field(
144
178
  default=None, serialization_alias="timeoutMinutes"
145
179
  )
@@ -168,6 +202,9 @@ class SandboxListParams(SandboxBaseModel):
168
202
  status: Optional[SandboxStatus] = Field(default=None, exclude=None)
169
203
  page: int = Field(default=1, ge=1)
170
204
  limit: int = Field(default=10, ge=1)
205
+ start: Optional[int] = None
206
+ end: Optional[int] = None
207
+ search: Optional[str] = None
171
208
 
172
209
 
173
210
  class SandboxListResponse(SandboxBaseModel):
@@ -212,6 +249,7 @@ class SandboxSnapshotSummary(SandboxBaseModel):
212
249
  class SandboxSnapshotListParams(SandboxBaseModel):
213
250
  status: Optional[SandboxSnapshotStatus] = Field(default=None, exclude=None)
214
251
  limit: Optional[int] = Field(default=None, ge=1)
252
+ image_name: Optional[str] = Field(default=None, serialization_alias="imageName")
215
253
 
216
254
 
217
255
  class SandboxSnapshotListResponse(SandboxBaseModel):
@@ -238,17 +276,6 @@ class SandboxMemorySnapshotResult(SandboxBaseModel):
238
276
  image_namespace: str = Field(alias="imageNamespace")
239
277
 
240
278
 
241
- class SandboxExposeParams(SandboxBaseModel):
242
- port: int
243
- auth: Optional[bool] = None
244
-
245
-
246
- class SandboxExposeResult(SandboxBaseModel):
247
- port: int
248
- auth: bool
249
- url: str
250
-
251
-
252
279
  class SandboxExecParams(SandboxBaseModel):
253
280
  command: str
254
281
  args: Optional[List[str]] = None
@@ -390,6 +417,9 @@ SandboxFileWriteData = Union[str, bytes]
390
417
  class SandboxFileWriteEntry(SandboxBaseModel):
391
418
  path: str
392
419
  data: SandboxFileWriteData
420
+ encoding: Optional[SandboxFileEncoding] = None
421
+ append: Optional[bool] = None
422
+ mode: Optional[str] = None
393
423
 
394
424
 
395
425
  class SandboxFileTextWriteOptions(SandboxBaseModel):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hyperbrowser"
3
- version = "0.88.2"
3
+ version = "0.89.0"
4
4
  description = "Python SDK for hyperbrowser"
5
5
  authors = ["Nikhil Shahi <nshahi1998@gmail.com>"]
6
6
  license = "MIT"
@@ -1,105 +0,0 @@
1
- # Hyperbrowser Python SDK
2
-
3
- Checkout the full documentation [here](https://hyperbrowser.ai/docs)
4
-
5
- ## Installation
6
-
7
- Currently Hyperbrowser supports creating a browser session in two ways:
8
-
9
- - Async Client
10
- - Sync Client
11
-
12
- It can be installed from `pypi` by running :
13
-
14
- ```shell
15
- pip install hyperbrowser
16
- ```
17
-
18
- ## Configuration
19
-
20
- Both the sync and async client follow similar configuration params
21
-
22
- ### API Key
23
- The API key can be configured either from the constructor arguments or environment variables using `HYPERBROWSER_API_KEY`
24
-
25
- ## Usage
26
-
27
- ### Async
28
-
29
- ```python
30
- import asyncio
31
- from pyppeteer import connect
32
- from hyperbrowser import AsyncHyperbrowser
33
-
34
- HYPERBROWSER_API_KEY = "test-key"
35
-
36
- async def main():
37
- async with AsyncHyperbrowser(api_key=HYPERBROWSER_API_KEY) as client:
38
- session = await client.sessions.create()
39
-
40
- ws_endpoint = session.ws_endpoint
41
- browser = await connect(browserWSEndpoint=ws_endpoint, defaultViewport=None)
42
-
43
- # Get pages
44
- pages = await browser.pages()
45
- if not pages:
46
- raise Exception("No pages available")
47
-
48
- page = pages[0]
49
-
50
- # Navigate to a website
51
- print("Navigating to Hacker News...")
52
- await page.goto("https://news.ycombinator.com/")
53
- page_title = await page.title()
54
- print("Page title:", page_title)
55
-
56
- await page.close()
57
- await browser.disconnect()
58
- await client.sessions.stop(session.id)
59
- print("Session completed!")
60
-
61
- # Run the asyncio event loop
62
- asyncio.get_event_loop().run_until_complete(main())
63
- ```
64
- ### Sync
65
-
66
- ```python
67
- from playwright.sync_api import sync_playwright
68
- from hyperbrowser import Hyperbrowser
69
-
70
- HYPERBROWSER_API_KEY = "test-key"
71
-
72
- def main():
73
- client = Hyperbrowser(api_key=HYPERBROWSER_API_KEY)
74
- session = client.sessions.create()
75
-
76
- ws_endpoint = session.ws_endpoint
77
-
78
- # Launch Playwright and connect to the remote browser
79
- with sync_playwright() as p:
80
- browser = p.chromium.connect_over_cdp(ws_endpoint)
81
- context = browser.new_context()
82
-
83
- # Get the first page or create a new one
84
- if len(context.pages) == 0:
85
- page = context.new_page()
86
- else:
87
- page = context.pages[0]
88
-
89
- # Navigate to a website
90
- print("Navigating to Hacker News...")
91
- page.goto("https://news.ycombinator.com/")
92
- page_title = page.title()
93
- print("Page title:", page_title)
94
-
95
- page.close()
96
- browser.close()
97
- print("Session completed!")
98
- client.sessions.stop(session.id)
99
-
100
- # Run the asyncio event loop
101
- main()
102
- ```
103
- ## License
104
-
105
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
File without changes