acontext 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl

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.
@@ -0,0 +1,436 @@
1
+ """Text editor file operations for sandbox environments."""
2
+
3
+ import base64
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from .sandbox import AsyncSandboxContext, SandboxContext
8
+
9
+
10
+ def escape_for_shell(s: str) -> str:
11
+ """Escape a string for safe use in shell commands."""
12
+ # Use single quotes and escape any single quotes in the string
13
+ return "'" + s.replace("'", "'\"'\"'") + "'"
14
+
15
+
16
+ # ============================================================================
17
+ # Sync Operations
18
+ # ============================================================================
19
+
20
+
21
+ def view_file(
22
+ ctx: "SandboxContext", path: str, view_range: list | None, timeout: float | None
23
+ ) -> dict:
24
+ """View file content with line numbers.
25
+
26
+ Args:
27
+ ctx: The sandbox context.
28
+ path: The file path to view.
29
+ view_range: Optional [start_line, end_line] to view specific lines.
30
+ timeout: Optional timeout for command execution.
31
+
32
+ Returns:
33
+ A dict with file content and metadata, or error information.
34
+ """
35
+ # First check if file exists and get total lines
36
+ check_cmd = f"wc -l < {escape_for_shell(path)} 2>/dev/null || echo 'FILE_NOT_FOUND'"
37
+ result = ctx.client.sandboxes.exec_command(
38
+ sandbox_id=ctx.sandbox_id,
39
+ command=check_cmd,
40
+ timeout=timeout,
41
+ )
42
+
43
+ if "FILE_NOT_FOUND" in result.stdout or result.exit_code != 0:
44
+ return {
45
+ "error": f"File not found: {path}",
46
+ "stderr": result.stderr,
47
+ }
48
+
49
+ total_lines = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
50
+
51
+ # Build the view command with line numbers
52
+ if view_range and len(view_range) == 2:
53
+ start_line, end_line = view_range
54
+ cmd = f"sed -n '{start_line},{end_line}p' {escape_for_shell(path)} | nl -ba -v {start_line}"
55
+ else:
56
+ cmd = f"nl -ba {escape_for_shell(path)}"
57
+ start_line = 1
58
+
59
+ result = ctx.client.sandboxes.exec_command(
60
+ sandbox_id=ctx.sandbox_id,
61
+ command=cmd,
62
+ timeout=timeout,
63
+ )
64
+
65
+ if result.exit_code != 0:
66
+ return {
67
+ "error": f"Failed to view file: {path}",
68
+ "stderr": result.stderr,
69
+ }
70
+
71
+ # Count lines in output
72
+ content_lines = (
73
+ result.stdout.rstrip("\n").split("\n") if result.stdout.strip() else []
74
+ )
75
+ num_lines = len(content_lines)
76
+
77
+ return {
78
+ "file_type": "text",
79
+ "content": result.stdout,
80
+ "numLines": num_lines,
81
+ "startLine": start_line if view_range else 1,
82
+ "totalLines": total_lines + 1, # wc -l doesn't count last line without newline
83
+ }
84
+
85
+
86
+ def create_file(
87
+ ctx: "SandboxContext", path: str, file_text: str, timeout: float | None
88
+ ) -> dict:
89
+ """Create a new file with content.
90
+
91
+ Args:
92
+ ctx: The sandbox context.
93
+ path: The file path to create.
94
+ file_text: The content to write to the file.
95
+ timeout: Optional timeout for command execution.
96
+
97
+ Returns:
98
+ A dict with creation status or error information.
99
+ """
100
+ # Check if file already exists
101
+ check_cmd = f"test -f {escape_for_shell(path)} && echo 'EXISTS' || echo 'NEW'"
102
+ check_result = ctx.client.sandboxes.exec_command(
103
+ sandbox_id=ctx.sandbox_id,
104
+ command=check_cmd,
105
+ timeout=timeout,
106
+ )
107
+ is_update = "EXISTS" in check_result.stdout
108
+
109
+ # Create directory if needed
110
+ dir_path = "/".join(path.split("/")[:-1])
111
+ if dir_path:
112
+ mkdir_cmd = f"mkdir -p {escape_for_shell(dir_path)}"
113
+ ctx.client.sandboxes.exec_command(
114
+ sandbox_id=ctx.sandbox_id,
115
+ command=mkdir_cmd,
116
+ timeout=timeout,
117
+ )
118
+
119
+ # Write file using base64 encoding to safely transfer content
120
+ encoded_content = base64.b64encode(file_text.encode()).decode()
121
+ write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
122
+
123
+ result = ctx.client.sandboxes.exec_command(
124
+ sandbox_id=ctx.sandbox_id,
125
+ command=write_cmd,
126
+ timeout=timeout,
127
+ )
128
+
129
+ if result.exit_code != 0:
130
+ return {
131
+ "error": f"Failed to create file: {path}",
132
+ "stderr": result.stderr,
133
+ }
134
+
135
+ return {
136
+ "is_file_update": is_update,
137
+ "message": f"File {'updated' if is_update else 'created'}: {path}",
138
+ }
139
+
140
+
141
+ def str_replace(
142
+ ctx: "SandboxContext", path: str, old_str: str, new_str: str, timeout: float | None
143
+ ) -> dict:
144
+ """Replace a string in a file.
145
+
146
+ Args:
147
+ ctx: The sandbox context.
148
+ path: The file path to modify.
149
+ old_str: The exact string to find and replace.
150
+ new_str: The string to replace old_str with.
151
+ timeout: Optional timeout for command execution.
152
+
153
+ Returns:
154
+ A dict with diff information or error details.
155
+ """
156
+ # First read the file content
157
+ read_cmd = f"cat {escape_for_shell(path)}"
158
+ result = ctx.client.sandboxes.exec_command(
159
+ sandbox_id=ctx.sandbox_id,
160
+ command=read_cmd,
161
+ timeout=timeout,
162
+ )
163
+
164
+ if result.exit_code != 0:
165
+ return {
166
+ "error": f"File not found: {path}",
167
+ "stderr": result.stderr,
168
+ }
169
+
170
+ original_content = result.stdout
171
+
172
+ # Check if old_str exists in the file
173
+ if old_str not in original_content:
174
+ return {
175
+ "error": f"String not found in file: {old_str[:50]}...",
176
+ }
177
+
178
+ # Count occurrences
179
+ occurrences = original_content.count(old_str)
180
+ if occurrences > 1:
181
+ return {
182
+ "error": f"Multiple occurrences ({occurrences}) of the string found. Please provide more context to make the match unique.",
183
+ }
184
+
185
+ # Perform the replacement
186
+ new_content = original_content.replace(old_str, new_str, 1)
187
+
188
+ # Find the line numbers affected
189
+ old_lines = original_content.split("\n")
190
+ new_lines = new_content.split("\n")
191
+
192
+ # Find where the change starts
193
+ old_start = 1
194
+ for i, (old_line, new_line) in enumerate(zip(old_lines, new_lines)):
195
+ if old_line != new_line:
196
+ old_start = i + 1
197
+ break
198
+
199
+ # Write the new content
200
+ encoded_content = base64.b64encode(new_content.encode()).decode()
201
+ write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
202
+
203
+ result = ctx.client.sandboxes.exec_command(
204
+ sandbox_id=ctx.sandbox_id,
205
+ command=write_cmd,
206
+ timeout=timeout,
207
+ )
208
+
209
+ if result.exit_code != 0:
210
+ return {
211
+ "error": f"Failed to write file: {path}",
212
+ "stderr": result.stderr,
213
+ }
214
+
215
+ # Calculate diff info
216
+ old_str_lines = old_str.count("\n") + 1
217
+ new_str_lines = new_str.count("\n") + 1
218
+
219
+ # Build diff lines
220
+ diff_lines = []
221
+ for line in old_str.split("\n"):
222
+ diff_lines.append(f"-{line}")
223
+ for line in new_str.split("\n"):
224
+ diff_lines.append(f"+{line}")
225
+
226
+ return {
227
+ "oldStart": old_start,
228
+ "oldLines": old_str_lines,
229
+ "newStart": old_start,
230
+ "newLines": new_str_lines,
231
+ "lines": diff_lines,
232
+ }
233
+
234
+
235
+ # ============================================================================
236
+ # Async Operations
237
+ # ============================================================================
238
+
239
+
240
+ async def async_view_file(
241
+ ctx: "AsyncSandboxContext", path: str, view_range: list | None, timeout: float | None
242
+ ) -> dict:
243
+ """View file content with line numbers (async).
244
+
245
+ Args:
246
+ ctx: The async sandbox context.
247
+ path: The file path to view.
248
+ view_range: Optional [start_line, end_line] to view specific lines.
249
+ timeout: Optional timeout for command execution.
250
+
251
+ Returns:
252
+ A dict with file content and metadata, or error information.
253
+ """
254
+ check_cmd = f"wc -l < {escape_for_shell(path)} 2>/dev/null || echo 'FILE_NOT_FOUND'"
255
+ result = await ctx.client.sandboxes.exec_command(
256
+ sandbox_id=ctx.sandbox_id,
257
+ command=check_cmd,
258
+ timeout=timeout,
259
+ )
260
+
261
+ if "FILE_NOT_FOUND" in result.stdout or result.exit_code != 0:
262
+ return {
263
+ "error": f"File not found: {path}",
264
+ "stderr": result.stderr,
265
+ }
266
+
267
+ total_lines = int(result.stdout.strip()) if result.stdout.strip().isdigit() else 0
268
+
269
+ if view_range and len(view_range) == 2:
270
+ start_line, end_line = view_range
271
+ cmd = f"sed -n '{start_line},{end_line}p' {escape_for_shell(path)} | nl -ba -v {start_line}"
272
+ else:
273
+ cmd = f"nl -ba {escape_for_shell(path)}"
274
+ start_line = 1
275
+
276
+ result = await ctx.client.sandboxes.exec_command(
277
+ sandbox_id=ctx.sandbox_id,
278
+ command=cmd,
279
+ timeout=timeout,
280
+ )
281
+
282
+ if result.exit_code != 0:
283
+ return {
284
+ "error": f"Failed to view file: {path}",
285
+ "stderr": result.stderr,
286
+ }
287
+
288
+ content_lines = (
289
+ result.stdout.rstrip("\n").split("\n") if result.stdout.strip() else []
290
+ )
291
+ num_lines = len(content_lines)
292
+
293
+ return {
294
+ "file_type": "text",
295
+ "content": result.stdout,
296
+ "numLines": num_lines,
297
+ "startLine": start_line if view_range else 1,
298
+ "totalLines": total_lines + 1,
299
+ }
300
+
301
+
302
+ async def async_create_file(
303
+ ctx: "AsyncSandboxContext", path: str, file_text: str, timeout: float | None
304
+ ) -> dict:
305
+ """Create a new file with content (async).
306
+
307
+ Args:
308
+ ctx: The async sandbox context.
309
+ path: The file path to create.
310
+ file_text: The content to write to the file.
311
+ timeout: Optional timeout for command execution.
312
+
313
+ Returns:
314
+ A dict with creation status or error information.
315
+ """
316
+ check_cmd = f"test -f {escape_for_shell(path)} && echo 'EXISTS' || echo 'NEW'"
317
+ check_result = await ctx.client.sandboxes.exec_command(
318
+ sandbox_id=ctx.sandbox_id,
319
+ command=check_cmd,
320
+ timeout=timeout,
321
+ )
322
+ is_update = "EXISTS" in check_result.stdout
323
+
324
+ dir_path = "/".join(path.split("/")[:-1])
325
+ if dir_path:
326
+ mkdir_cmd = f"mkdir -p {escape_for_shell(dir_path)}"
327
+ await ctx.client.sandboxes.exec_command(
328
+ sandbox_id=ctx.sandbox_id,
329
+ command=mkdir_cmd,
330
+ timeout=timeout,
331
+ )
332
+
333
+ encoded_content = base64.b64encode(file_text.encode()).decode()
334
+ write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
335
+
336
+ result = await ctx.client.sandboxes.exec_command(
337
+ sandbox_id=ctx.sandbox_id,
338
+ command=write_cmd,
339
+ timeout=timeout,
340
+ )
341
+
342
+ if result.exit_code != 0:
343
+ return {
344
+ "error": f"Failed to create file: {path}",
345
+ "stderr": result.stderr,
346
+ }
347
+
348
+ return {
349
+ "is_file_update": is_update,
350
+ "message": f"File {'updated' if is_update else 'created'}: {path}",
351
+ }
352
+
353
+
354
+ async def async_str_replace(
355
+ ctx: "AsyncSandboxContext", path: str, old_str: str, new_str: str, timeout: float | None
356
+ ) -> dict:
357
+ """Replace a string in a file (async).
358
+
359
+ Args:
360
+ ctx: The async sandbox context.
361
+ path: The file path to modify.
362
+ old_str: The exact string to find and replace.
363
+ new_str: The string to replace old_str with.
364
+ timeout: Optional timeout for command execution.
365
+
366
+ Returns:
367
+ A dict with diff information or error details.
368
+ """
369
+ read_cmd = f"cat {escape_for_shell(path)}"
370
+ result = await ctx.client.sandboxes.exec_command(
371
+ sandbox_id=ctx.sandbox_id,
372
+ command=read_cmd,
373
+ timeout=timeout,
374
+ )
375
+
376
+ if result.exit_code != 0:
377
+ return {
378
+ "error": f"File not found: {path}",
379
+ "stderr": result.stderr,
380
+ }
381
+
382
+ original_content = result.stdout
383
+
384
+ if old_str not in original_content:
385
+ return {
386
+ "error": f"String not found in file: {old_str[:50]}...",
387
+ }
388
+
389
+ occurrences = original_content.count(old_str)
390
+ if occurrences > 1:
391
+ return {
392
+ "error": f"Multiple occurrences ({occurrences}) of the string found. Please provide more context to make the match unique.",
393
+ }
394
+
395
+ new_content = original_content.replace(old_str, new_str, 1)
396
+
397
+ old_lines = original_content.split("\n")
398
+ new_lines = new_content.split("\n")
399
+
400
+ old_start = 1
401
+ for i, (old_line, new_line) in enumerate(zip(old_lines, new_lines)):
402
+ if old_line != new_line:
403
+ old_start = i + 1
404
+ break
405
+
406
+ encoded_content = base64.b64encode(new_content.encode()).decode()
407
+ write_cmd = f"echo {escape_for_shell(encoded_content)} | base64 -d > {escape_for_shell(path)}"
408
+
409
+ result = await ctx.client.sandboxes.exec_command(
410
+ sandbox_id=ctx.sandbox_id,
411
+ command=write_cmd,
412
+ timeout=timeout,
413
+ )
414
+
415
+ if result.exit_code != 0:
416
+ return {
417
+ "error": f"Failed to write file: {path}",
418
+ "stderr": result.stderr,
419
+ }
420
+
421
+ old_str_lines = old_str.count("\n") + 1
422
+ new_str_lines = new_str.count("\n") + 1
423
+
424
+ diff_lines = []
425
+ for line in old_str.split("\n"):
426
+ diff_lines.append(f"-{line}")
427
+ for line in new_str.split("\n"):
428
+ diff_lines.append(f"+{line}")
429
+
430
+ return {
431
+ "oldStart": old_start,
432
+ "oldLines": old_str_lines,
433
+ "newStart": old_start,
434
+ "newLines": new_str_lines,
435
+ "lines": diff_lines,
436
+ }
acontext/async_client.py CHANGED
@@ -13,10 +13,8 @@ from .errors import APIError, TransportError
13
13
  from .messages import MessagePart as MessagePart
14
14
  from .uploads import FileUpload as FileUpload
15
15
  from .resources.async_disks import AsyncDisksAPI as AsyncDisksAPI
16
- from .resources.async_blocks import AsyncBlocksAPI as AsyncBlocksAPI
17
16
  from .resources.async_sandboxes import AsyncSandboxesAPI as AsyncSandboxesAPI
18
17
  from .resources.async_sessions import AsyncSessionsAPI as AsyncSessionsAPI
19
- from .resources.async_spaces import AsyncSpacesAPI as AsyncSpacesAPI
20
18
  from .resources.async_tools import AsyncToolsAPI as AsyncToolsAPI
21
19
  from .resources.async_skills import AsyncSkillsAPI as AsyncSkillsAPI
22
20
  from .resources.async_users import AsyncUsersAPI as AsyncUsersAPI
@@ -106,11 +104,9 @@ class AcontextAsyncClient:
106
104
 
107
105
  self._timeout = actual_timeout
108
106
 
109
- self.spaces = AsyncSpacesAPI(self)
110
107
  self.sessions = AsyncSessionsAPI(self)
111
108
  self.disks = AsyncDisksAPI(self)
112
109
  self.artifacts = self.disks.artifacts
113
- self.blocks = AsyncBlocksAPI(self)
114
110
  self.tools = AsyncToolsAPI(self)
115
111
  self.skills = AsyncSkillsAPI(self)
116
112
  self.users = AsyncUsersAPI(self)
@@ -160,7 +156,10 @@ class AcontextAsyncClient:
160
156
  data: Mapping[str, Any] | None = None,
161
157
  files: Mapping[str, tuple[str, BinaryIO, str | None]] | None = None,
162
158
  unwrap: bool = True,
159
+ timeout: float | None = None,
163
160
  ) -> Any:
161
+ # Use per-request timeout if provided, otherwise use client default
162
+ effective_timeout = timeout if timeout is not None else self._timeout
164
163
  try:
165
164
  response = await self._client.request(
166
165
  method=method,
@@ -169,7 +168,7 @@ class AcontextAsyncClient:
169
168
  json=json_data,
170
169
  data=data,
171
170
  files=files,
172
- timeout=self._timeout,
171
+ timeout=effective_timeout,
173
172
  )
174
173
  except httpx.HTTPError as exc: # pragma: no cover - passthrough to caller
175
174
  raise TransportError(str(exc)) from exc
acontext/client.py CHANGED
@@ -13,10 +13,8 @@ from .errors import APIError, TransportError
13
13
  from .messages import MessagePart as MessagePart
14
14
  from .uploads import FileUpload as FileUpload
15
15
  from .resources.disks import DisksAPI as DisksAPI
16
- from .resources.blocks import BlocksAPI as BlocksAPI
17
16
  from .resources.sandboxes import SandboxesAPI as SandboxesAPI
18
17
  from .resources.sessions import SessionsAPI as SessionsAPI
19
- from .resources.spaces import SpacesAPI as SpacesAPI
20
18
  from .resources.tools import ToolsAPI as ToolsAPI
21
19
  from .resources.skills import SkillsAPI as SkillsAPI
22
20
  from .resources.users import UsersAPI as UsersAPI
@@ -106,11 +104,9 @@ class AcontextClient:
106
104
 
107
105
  self._timeout = actual_timeout
108
106
 
109
- self.spaces = SpacesAPI(self)
110
107
  self.sessions = SessionsAPI(self)
111
108
  self.disks = DisksAPI(self)
112
109
  self.artifacts = self.disks.artifacts
113
- self.blocks = BlocksAPI(self)
114
110
  self.tools = ToolsAPI(self)
115
111
  self.skills = SkillsAPI(self)
116
112
  self.users = UsersAPI(self)
@@ -159,7 +155,10 @@ class AcontextClient:
159
155
  data: Mapping[str, Any] | None = None,
160
156
  files: Mapping[str, tuple[str, BinaryIO, str | None]] | None = None,
161
157
  unwrap: bool = True,
158
+ timeout: float | None = None,
162
159
  ) -> Any:
160
+ # Use per-request timeout if provided, otherwise use client default
161
+ effective_timeout = timeout if timeout is not None else self._timeout
163
162
  try:
164
163
  response = self._client.request(
165
164
  method=method,
@@ -168,7 +167,7 @@ class AcontextClient:
168
167
  json=json_data,
169
168
  data=data,
170
169
  files=files,
171
- timeout=self._timeout,
170
+ timeout=effective_timeout,
172
171
  )
173
172
  except httpx.HTTPError as exc: # pragma: no cover - passthrough to caller
174
173
  raise TransportError(str(exc)) from exc
acontext/client_types.py CHANGED
@@ -17,6 +17,7 @@ class RequesterProtocol(Protocol):
17
17
  data: Mapping[str, Any] | None = None,
18
18
  files: Mapping[str, tuple[str, BinaryIO, str | None]] | None = None,
19
19
  unwrap: bool = True,
20
+ timeout: float | None = None,
20
21
  ) -> Any:
21
22
  ...
22
23
 
@@ -32,5 +33,6 @@ class AsyncRequesterProtocol(Protocol):
32
33
  data: Mapping[str, Any] | None = None,
33
34
  files: Mapping[str, tuple[str, BinaryIO, str | None]] | None = None,
34
35
  unwrap: bool = True,
36
+ timeout: float | None = None,
35
37
  ) -> Awaitable[Any]:
36
38
  ...
@@ -1,18 +1,14 @@
1
1
  """Resource-specific API helpers for the Acontext client."""
2
2
 
3
- from .async_blocks import AsyncBlocksAPI
4
3
  from .async_disks import AsyncDisksAPI, AsyncDiskArtifactsAPI
5
4
  from .async_sandboxes import AsyncSandboxesAPI
6
5
  from .async_sessions import AsyncSessionsAPI
7
- from .async_spaces import AsyncSpacesAPI
8
6
  from .async_tools import AsyncToolsAPI
9
7
  from .async_skills import AsyncSkillsAPI
10
8
  from .async_users import AsyncUsersAPI
11
- from .blocks import BlocksAPI
12
9
  from .disks import DisksAPI, DiskArtifactsAPI
13
10
  from .sandboxes import SandboxesAPI
14
11
  from .sessions import SessionsAPI
15
- from .spaces import SpacesAPI
16
12
  from .tools import ToolsAPI
17
13
  from .skills import SkillsAPI
18
14
  from .users import UsersAPI
@@ -20,19 +16,15 @@ from .users import UsersAPI
20
16
  __all__ = [
21
17
  "DisksAPI",
22
18
  "DiskArtifactsAPI",
23
- "BlocksAPI",
24
19
  "SandboxesAPI",
25
20
  "SessionsAPI",
26
- "SpacesAPI",
27
21
  "ToolsAPI",
28
22
  "SkillsAPI",
29
23
  "UsersAPI",
30
24
  "AsyncDisksAPI",
31
25
  "AsyncDiskArtifactsAPI",
32
- "AsyncBlocksAPI",
33
26
  "AsyncSandboxesAPI",
34
27
  "AsyncSessionsAPI",
35
- "AsyncSpacesAPI",
36
28
  "AsyncToolsAPI",
37
29
  "AsyncSkillsAPI",
38
30
  "AsyncUsersAPI",
@@ -2,8 +2,10 @@
2
2
  Sandboxes endpoints (async).
3
3
  """
4
4
 
5
+ from .._utils import build_params
5
6
  from ..client_types import AsyncRequesterProtocol
6
7
  from ..types.sandbox import (
8
+ GetSandboxLogsOutput,
7
9
  SandboxCommandOutput,
8
10
  SandboxRuntimeInfo,
9
11
  )
@@ -28,12 +30,15 @@ class AsyncSandboxesAPI:
28
30
  *,
29
31
  sandbox_id: str,
30
32
  command: str,
33
+ timeout: float | None = None,
31
34
  ) -> SandboxCommandOutput:
32
35
  """Execute a shell command in the sandbox.
33
36
 
34
37
  Args:
35
38
  sandbox_id: The UUID of the sandbox.
36
39
  command: The shell command to execute.
40
+ timeout: Optional timeout in seconds for this command.
41
+ If not provided, uses the client's default timeout.
37
42
 
38
43
  Returns:
39
44
  SandboxCommandOutput containing stdout, stderr, and exit code.
@@ -42,6 +47,7 @@ class AsyncSandboxesAPI:
42
47
  "POST",
43
48
  f"/sandbox/{sandbox_id}/exec",
44
49
  json_data={"command": command},
50
+ timeout=timeout,
45
51
  )
46
52
  return SandboxCommandOutput.model_validate(data)
47
53
 
@@ -56,3 +62,24 @@ class AsyncSandboxesAPI:
56
62
  """
57
63
  data = await self._requester.request("DELETE", f"/sandbox/{sandbox_id}")
58
64
  return FlagResponse.model_validate(data)
65
+
66
+ async def get_logs(
67
+ self,
68
+ *,
69
+ limit: int | None = None,
70
+ cursor: str | None = None,
71
+ time_desc: bool | None = None,
72
+ ) -> GetSandboxLogsOutput:
73
+ """Get sandbox logs for the project with cursor-based pagination.
74
+
75
+ Args:
76
+ limit: Maximum number of logs to return (default 20, max 200).
77
+ cursor: Cursor for pagination. Use the cursor from the previous response to get the next page.
78
+ time_desc: Order by created_at descending if True, ascending if False (default False).
79
+
80
+ Returns:
81
+ GetSandboxLogsOutput containing the list of sandbox logs and pagination information.
82
+ """
83
+ params = build_params(limit=limit, cursor=cursor, time_desc=time_desc)
84
+ data = await self._requester.request("GET", "/sandbox/logs", params=params or None)
85
+ return GetSandboxLogsOutput.model_validate(data)