blaxel 0.2.26rc119__py3-none-any.whl → 0.2.28__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.
Files changed (118) hide show
  1. blaxel/__init__.py +2 -2
  2. blaxel/core/__init__.py +9 -1
  3. blaxel/core/client/api/{privateclusters/create_private_cluster.py → images/cleanup_images.py} +31 -25
  4. blaxel/core/client/models/__init__.py +2 -4
  5. blaxel/core/client/models/agent.py +1 -0
  6. blaxel/core/client/models/agent_spec.py +1 -24
  7. blaxel/core/client/models/billable_time_metric.py +1 -0
  8. blaxel/core/{sandbox/client/models/find_match.py → client/models/cleanup_images_response_200.py} +19 -19
  9. blaxel/core/client/models/configuration.py +1 -0
  10. blaxel/core/client/models/core_event.py +9 -0
  11. blaxel/core/client/models/core_spec.py +1 -24
  12. blaxel/core/client/models/core_spec_configurations.py +1 -0
  13. blaxel/core/client/models/create_job_execution_request.py +1 -0
  14. blaxel/core/client/models/create_job_execution_response.py +1 -0
  15. blaxel/core/client/models/custom_domain.py +1 -0
  16. blaxel/core/client/models/custom_domain_metadata.py +1 -0
  17. blaxel/core/client/models/custom_domain_spec.py +1 -0
  18. blaxel/core/client/models/delete_volume_template_version_response_200.py +1 -0
  19. blaxel/core/client/models/entrypoint.py +1 -0
  20. blaxel/core/client/models/form.py +1 -0
  21. blaxel/core/client/models/function.py +1 -0
  22. blaxel/core/client/models/function_spec.py +1 -24
  23. blaxel/core/client/models/image.py +1 -0
  24. blaxel/core/client/models/image_spec.py +1 -0
  25. blaxel/core/client/models/integration.py +1 -0
  26. blaxel/core/client/models/integration_connection.py +1 -0
  27. blaxel/core/client/models/integration_connection_spec.py +1 -0
  28. blaxel/core/client/models/integration_endpoint.py +1 -0
  29. blaxel/core/client/models/integration_endpoints.py +2 -0
  30. blaxel/core/client/models/job.py +1 -0
  31. blaxel/core/client/models/job_execution.py +1 -0
  32. blaxel/core/client/models/job_execution_spec.py +1 -0
  33. blaxel/core/client/models/job_execution_task.py +1 -0
  34. blaxel/core/client/models/job_metrics.py +1 -0
  35. blaxel/core/client/models/job_spec.py +1 -24
  36. blaxel/core/client/models/jobs_network_chart.py +1 -0
  37. blaxel/core/client/models/jobs_success_failed_chart.py +1 -0
  38. blaxel/core/client/models/latency_metric.py +1 -0
  39. blaxel/core/client/models/location_response.py +1 -0
  40. blaxel/core/client/models/mcp_definition.py +1 -0
  41. blaxel/core/client/models/metadata.py +1 -0
  42. blaxel/core/client/models/metrics.py +34 -0
  43. blaxel/core/client/models/model.py +1 -0
  44. blaxel/core/client/models/model_spec.py +1 -24
  45. blaxel/core/client/models/pending_invitation_accept.py +1 -0
  46. blaxel/core/client/models/pending_invitation_render.py +1 -0
  47. blaxel/core/client/models/policy.py +1 -0
  48. blaxel/core/client/models/policy_spec.py +1 -0
  49. blaxel/core/client/models/preview.py +1 -0
  50. blaxel/core/client/models/preview_spec.py +1 -0
  51. blaxel/core/client/models/preview_token.py +1 -0
  52. blaxel/core/client/models/public_ips.py +1 -0
  53. blaxel/core/client/models/request_duration_over_time_metrics.py +1 -0
  54. blaxel/core/client/models/request_total_by_origin_metric.py +1 -0
  55. blaxel/core/client/models/request_total_metric.py +1 -0
  56. blaxel/core/client/models/resource_metrics.py +1 -0
  57. blaxel/core/client/models/revision_configuration.py +9 -0
  58. blaxel/core/client/models/runtime.py +1 -0
  59. blaxel/core/client/models/sandbox.py +1 -0
  60. blaxel/core/client/models/sandbox_definition.py +1 -0
  61. blaxel/core/client/models/sandbox_lifecycle.py +1 -0
  62. blaxel/core/client/models/sandbox_spec.py +1 -24
  63. blaxel/core/client/models/serverless_config.py +1 -0
  64. blaxel/core/client/models/start_sandbox.py +1 -0
  65. blaxel/core/client/models/stop_sandbox.py +1 -0
  66. blaxel/core/client/models/store_agent.py +1 -0
  67. blaxel/core/client/models/store_configuration.py +1 -0
  68. blaxel/core/client/models/template.py +1 -0
  69. blaxel/core/client/models/time_to_first_token_over_time_metrics.py +1 -0
  70. blaxel/core/client/models/token_rate_metrics.py +1 -0
  71. blaxel/core/client/models/trigger.py +10 -0
  72. blaxel/core/client/models/trigger_configuration.py +28 -0
  73. blaxel/core/client/models/volume.py +1 -0
  74. blaxel/core/client/models/volume_template.py +1 -0
  75. blaxel/core/client/models/websocket_channel.py +9 -0
  76. blaxel/core/client/models/workspace.py +1 -0
  77. blaxel/core/client/response_interceptor.py +0 -1
  78. blaxel/core/common/__init__.py +10 -0
  79. blaxel/core/common/autoload.py +5 -4
  80. blaxel/core/common/settings.py +29 -0
  81. blaxel/core/common/webhook.py +187 -0
  82. blaxel/core/jobs/__init__.py +355 -8
  83. blaxel/core/sandbox/client/models/__init__.py +0 -16
  84. blaxel/core/sandbox/default/action.py +10 -27
  85. blaxel/core/sandbox/default/filesystem.py +52 -187
  86. blaxel/core/sandbox/default/interpreter.py +55 -62
  87. blaxel/core/sandbox/default/process.py +46 -66
  88. blaxel/core/sandbox/sync/filesystem.py +5 -2
  89. blaxel/googleadk/model.py +0 -1
  90. blaxel/livekit/model.py +0 -1
  91. {blaxel-0.2.26rc119.dist-info → blaxel-0.2.28.dist-info}/METADATA +2 -2
  92. {blaxel-0.2.26rc119.dist-info → blaxel-0.2.28.dist-info}/RECORD +94 -117
  93. blaxel/core/client/api/privateclusters/__init__.py +0 -0
  94. blaxel/core/client/api/privateclusters/delete_private_cluster.py +0 -152
  95. blaxel/core/client/api/privateclusters/get_private_cluster.py +0 -155
  96. blaxel/core/client/api/privateclusters/get_private_cluster_health.py +0 -97
  97. blaxel/core/client/api/privateclusters/list_private_clusters.py +0 -136
  98. blaxel/core/client/api/privateclusters/update_private_cluster.py +0 -152
  99. blaxel/core/client/api/privateclusters/update_private_cluster_health.py +0 -97
  100. blaxel/core/client/models/model_private_cluster.py +0 -79
  101. blaxel/core/client/models/private_cluster.py +0 -183
  102. blaxel/core/sandbox/client/api/filesystem/delete_filesystem_tree_path.py +0 -188
  103. blaxel/core/sandbox/client/api/filesystem/get_filesystem_content_search_path.py +0 -265
  104. blaxel/core/sandbox/client/api/filesystem/get_filesystem_find_path.py +0 -248
  105. blaxel/core/sandbox/client/api/filesystem/get_filesystem_search_path.py +0 -237
  106. blaxel/core/sandbox/client/api/filesystem/get_filesystem_tree_path.py +0 -197
  107. blaxel/core/sandbox/client/api/filesystem/put_filesystem_tree_path.py +0 -223
  108. blaxel/core/sandbox/client/api/websocket/__init__.py +0 -0
  109. blaxel/core/sandbox/client/api/websocket/get_ws.py +0 -81
  110. blaxel/core/sandbox/client/models/content_search_match.py +0 -98
  111. blaxel/core/sandbox/client/models/content_search_response.py +0 -97
  112. blaxel/core/sandbox/client/models/find_response.py +0 -88
  113. blaxel/core/sandbox/client/models/fuzzy_search_match.py +0 -78
  114. blaxel/core/sandbox/client/models/fuzzy_search_response.py +0 -88
  115. blaxel/core/sandbox/client/models/tree_request.py +0 -76
  116. blaxel/core/sandbox/client/models/tree_request_files.py +0 -49
  117. {blaxel-0.2.26rc119.dist-info → blaxel-0.2.28.dist-info}/WHEEL +0 -0
  118. {blaxel-0.2.26rc119.dist-info → blaxel-0.2.28.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,187 @@
1
+ """Webhook signature verification for async-sidecar callbacks."""
2
+
3
+ import hashlib
4
+ import hmac
5
+ import time
6
+ from typing import Protocol
7
+
8
+
9
+ class RequestLike(Protocol):
10
+ """Protocol for request-like objects with body and headers."""
11
+
12
+ @property
13
+ def body(self) -> bytes:
14
+ """Raw request body as bytes."""
15
+ ...
16
+
17
+ @property
18
+ def headers(self) -> dict[str, str]:
19
+ """Request headers as dictionary."""
20
+ ...
21
+
22
+
23
+ class AsyncSidecarCallback:
24
+ """Callback payload from async-sidecar."""
25
+
26
+ def __init__(
27
+ self,
28
+ status_code: int,
29
+ response_body: str,
30
+ response_length: int,
31
+ timestamp: int,
32
+ ):
33
+ self.status_code = status_code
34
+ self.response_body = response_body
35
+ self.response_length = response_length
36
+ self.timestamp = timestamp
37
+
38
+
39
+ def verify_webhook_signature(
40
+ body: bytes | str,
41
+ signature: str,
42
+ secret: str,
43
+ timestamp: str | None = None,
44
+ max_age: int = 300,
45
+ ) -> bool:
46
+ """
47
+ Verify the HMAC-SHA256 signature of a webhook callback from async-sidecar.
48
+
49
+ Args:
50
+ body: The raw request body (bytes or string)
51
+ signature: The X-Blaxel-Signature header value (format: "sha256=<hex_digest>")
52
+ secret: The secret key used to sign the webhook (same as CALLBACK_SECRET in async-sidecar)
53
+ timestamp: Optional X-Blaxel-Timestamp header value for replay attack prevention
54
+ max_age: Maximum age of the webhook in seconds (default: 300 = 5 minutes)
55
+
56
+ Returns:
57
+ True if the signature is valid, False otherwise
58
+
59
+ Example:
60
+ ```python
61
+ from blaxel.core import verify_webhook_signature
62
+ from flask import Flask, request
63
+
64
+ app = Flask(__name__)
65
+
66
+ @app.route('/webhook', methods=['POST'])
67
+ def webhook():
68
+ is_valid = verify_webhook_signature(
69
+ body=request.get_data(),
70
+ signature=request.headers.get('X-Blaxel-Signature', ''),
71
+ secret='your-callback-secret'
72
+ )
73
+
74
+ if not is_valid:
75
+ return {'error': 'Invalid signature'}, 401
76
+
77
+ data = request.json
78
+ # Process callback...
79
+ return {'received': True}
80
+ ```
81
+ """
82
+ if not body or not signature or not secret:
83
+ return False
84
+
85
+ try:
86
+ # Verify timestamp if provided (prevents replay attacks)
87
+ if timestamp:
88
+ request_time = int(timestamp)
89
+ current_time = int(time.time())
90
+ age = abs(current_time - request_time)
91
+
92
+ if age > max_age:
93
+ return False
94
+
95
+ # Convert body to bytes if string
96
+ body_bytes = body.encode("utf-8") if isinstance(body, str) else body
97
+
98
+ # Extract hex signature from "sha256=<hex>" format
99
+ expected_signature = signature.replace("sha256=", "")
100
+
101
+ # Compute HMAC-SHA256 signature
102
+ computed_signature = hmac.new(
103
+ secret.encode("utf-8"), body_bytes, hashlib.sha256
104
+ ).hexdigest()
105
+
106
+ # Timing-safe comparison to prevent timing attacks
107
+ return hmac.compare_digest(expected_signature, computed_signature)
108
+
109
+ except (ValueError, TypeError):
110
+ # Invalid signature format or other error
111
+ return False
112
+
113
+
114
+ def verify_webhook_from_request(
115
+ request: RequestLike,
116
+ secret: str,
117
+ max_age: int = 300,
118
+ ) -> bool:
119
+ """
120
+ Helper to verify webhook from a request object (Flask, FastAPI, etc.).
121
+
122
+ Args:
123
+ request: Request object with `body` and `headers` attributes
124
+ secret: The callback secret
125
+ max_age: Optional maximum age in seconds (default: 300)
126
+
127
+ Returns:
128
+ True if the signature is valid, False otherwise
129
+
130
+ Example with Flask:
131
+ ```python
132
+ from blaxel.core import verify_webhook_from_request
133
+ from flask import Flask, request
134
+
135
+ app = Flask(__name__)
136
+
137
+ @app.route('/webhook', methods=['POST'])
138
+ def webhook():
139
+ if not verify_webhook_from_request(request, 'your-callback-secret'):
140
+ return {'error': 'Invalid signature'}, 401
141
+
142
+ data = request.json
143
+ print(f"Received callback: {data}")
144
+ return {'received': True}
145
+ ```
146
+
147
+ Example with FastAPI:
148
+ ```python
149
+ from blaxel.core import verify_webhook_signature
150
+ from fastapi import FastAPI, Request, HTTPException
151
+
152
+ app = FastAPI()
153
+
154
+ @app.post('/webhook')
155
+ async def webhook(request: Request):
156
+ body = await request.body()
157
+ signature = request.headers.get('x-blaxel-signature', '')
158
+
159
+ if not verify_webhook_signature(body, signature, 'your-callback-secret'):
160
+ raise HTTPException(status_code=401, detail='Invalid signature')
161
+
162
+ data = await request.json()
163
+ return {'received': True}
164
+ ```
165
+ """
166
+ signature = request.headers.get("x-blaxel-signature", "")
167
+ timestamp = request.headers.get("x-blaxel-timestamp")
168
+
169
+ if not signature:
170
+ return False
171
+
172
+ return verify_webhook_signature(
173
+ body=request.body,
174
+ signature=signature,
175
+ secret=secret,
176
+ timestamp=timestamp,
177
+ max_age=max_age,
178
+ )
179
+
180
+
181
+ __all__ = [
182
+ "verify_webhook_signature",
183
+ "verify_webhook_from_request",
184
+ "AsyncSidecarCallback",
185
+ "RequestLike",
186
+ ]
187
+
@@ -1,13 +1,21 @@
1
1
  import argparse
2
2
  import asyncio
3
3
  import os
4
- import sys
4
+ import time
5
5
  from logging import getLogger
6
- from typing import Any, Awaitable, Callable, Dict
6
+ from typing import Any, Callable, Dict, List
7
7
 
8
8
  import requests
9
9
 
10
10
  from ..client import client
11
+ from ..client.api.jobs import (
12
+ create_job_execution,
13
+ delete_job_execution,
14
+ get_job_execution,
15
+ list_job_executions,
16
+ )
17
+ from ..client.models.create_job_execution_request import CreateJobExecutionRequest
18
+ from ..client.models.job_execution import JobExecution
11
19
  from ..common.internal import get_forced_url, get_global_unique_hash
12
20
  from ..common.settings import settings
13
21
 
@@ -27,7 +35,7 @@ class BlJobWrapper:
27
35
  args_dict[key] = unknown[i + 1]
28
36
  return args_dict
29
37
 
30
- response = requests.get(os.getenv("BL_EXECUTION_DATA_URL"))
38
+ response = requests.get(os.getenv("BL_EXECUTION_DATA_URL") or "")
31
39
  data = response.json()
32
40
  tasks = data.get("tasks", [])
33
41
  return tasks[self.index] if self.index < len(tasks) else {}
@@ -54,8 +62,7 @@ class BlJobWrapper:
54
62
  else:
55
63
  func(**parsed_args)
56
64
  except Exception as error:
57
- print("Job execution failed:", error, file=sys.stderr)
58
- sys.exit(1)
65
+ logger.error(f"Job execution failed: {error}")
59
66
 
60
67
 
61
68
  logger = getLogger(__name__)
@@ -96,7 +103,7 @@ class BlJob:
96
103
 
97
104
  def call(self, url, input_data, headers: dict = {}, params: dict = {}):
98
105
  body = {"tasks": input_data}
99
-
106
+
100
107
  # Merge settings headers with provided headers
101
108
  merged_headers = {**settings.headers, "Content-Type": "application/json", **headers}
102
109
 
@@ -110,7 +117,7 @@ class BlJob:
110
117
  async def acall(self, url, input_data, headers: dict = {}, params: dict = {}):
111
118
  logger.debug(f"Job Calling: {self.name}")
112
119
  body = {"tasks": input_data}
113
-
120
+
114
121
  # Merge settings headers with provided headers
115
122
  merged_headers = {**settings.headers, "Content-Type": "application/json", **headers}
116
123
 
@@ -136,7 +143,7 @@ class BlJob:
136
143
  )
137
144
  return response.text
138
145
 
139
- async def arun(self, input: Any, headers: dict = {}, params: dict = {}) -> Awaitable[str]:
146
+ async def arun(self, input: Any, headers: dict = {}, params: dict = {}) -> str:
140
147
  logger.debug(f"Job Calling: {self.name}")
141
148
  response = await self.acall(self.url, input, headers, params)
142
149
  if response.status_code >= 400:
@@ -151,6 +158,346 @@ class BlJob:
151
158
  )
152
159
  return response.text
153
160
 
161
+ def create_execution(self, request: CreateJobExecutionRequest) -> str:
162
+ """
163
+ Create a new execution for this job and return the execution ID.
164
+
165
+ Args:
166
+ request: The job execution request containing tasks and optional execution ID
167
+
168
+ Returns:
169
+ str: The execution ID
170
+
171
+ Raises:
172
+ Exception: If no execution ID is returned or the request fails
173
+ """
174
+ logger.debug(f"Creating execution for job: {self.name}")
175
+
176
+ response = create_job_execution.sync_detailed(
177
+ job_id=self.name,
178
+ client=client,
179
+ body=request,
180
+ )
181
+
182
+ if response.status_code != 200:
183
+ raise Exception(f"Failed to create job execution: {response.status_code}")
184
+
185
+ # The API returns executionId at the root level
186
+ if response.parsed and hasattr(response.parsed, "to_dict"):
187
+ response_dict = response.parsed.to_dict()
188
+ else:
189
+ # Parse the raw response to get executionId
190
+ import json
191
+
192
+ response_dict = json.loads(response.content)
193
+
194
+ # Check for both camelCase (API) and snake_case (parsed) formats
195
+ execution_id = response_dict.get("execution_id") or response_dict.get("executionId")
196
+ if not execution_id:
197
+ raise Exception("No execution ID returned from create job execution")
198
+
199
+ logger.debug(f"Created execution: {execution_id}")
200
+ return execution_id
201
+
202
+ async def acreate_execution(self, request: CreateJobExecutionRequest) -> str:
203
+ """
204
+ Create a new execution for this job and return the execution ID (async).
205
+
206
+ Args:
207
+ request: The job execution request containing tasks and optional execution ID
208
+
209
+ Returns:
210
+ str: The execution ID
211
+
212
+ Raises:
213
+ Exception: If no execution ID is returned or the request fails
214
+ """
215
+ logger.debug(f"Creating execution for job: {self.name}")
216
+
217
+ response = await create_job_execution.asyncio_detailed(
218
+ job_id=self.name,
219
+ client=client,
220
+ body=request,
221
+ )
222
+
223
+ if response.status_code != 200:
224
+ raise Exception(f"Failed to create job execution: {response.status_code}")
225
+
226
+ # The API returns executionId at the root level
227
+ if response.parsed and hasattr(response.parsed, "to_dict"):
228
+ response_dict = response.parsed.to_dict()
229
+ else:
230
+ # Parse the raw response to get executionId
231
+ import json
232
+
233
+ response_dict = json.loads(response.content)
234
+
235
+ # Check for both camelCase (API) and snake_case (parsed) formats
236
+ execution_id = response_dict.get("execution_id") or response_dict.get("executionId")
237
+ if not execution_id:
238
+ raise Exception("No execution ID returned from create job execution")
239
+
240
+ logger.debug(f"Created execution: {execution_id}")
241
+ return execution_id
242
+
243
+ def get_execution(self, execution_id: str) -> JobExecution:
244
+ """
245
+ Get a specific execution by ID.
246
+
247
+ Args:
248
+ execution_id: The execution ID
249
+
250
+ Returns:
251
+ JobExecution: The execution object
252
+
253
+ Raises:
254
+ Exception: If the execution is not found or the request fails
255
+ """
256
+ logger.debug(f"Getting execution {execution_id} for job: {self.name}")
257
+
258
+ response = get_job_execution.sync_detailed(
259
+ job_id=self.name,
260
+ execution_id=execution_id,
261
+ client=client,
262
+ )
263
+
264
+ if response.status_code == 404:
265
+ raise Exception(f"Execution '{execution_id}' not found for job '{self.name}'")
266
+
267
+ if response.status_code != 200:
268
+ raise Exception(f"Failed to get job execution: {response.status_code}")
269
+
270
+ if not response.parsed:
271
+ raise Exception("No execution data returned")
272
+
273
+ return response.parsed
274
+
275
+ async def aget_execution(self, execution_id: str) -> JobExecution:
276
+ """
277
+ Get a specific execution by ID (async).
278
+
279
+ Args:
280
+ execution_id: The execution ID
281
+
282
+ Returns:
283
+ JobExecution: The execution object
284
+
285
+ Raises:
286
+ Exception: If the execution is not found or the request fails
287
+ """
288
+ logger.debug(f"Getting execution {execution_id} for job: {self.name}")
289
+
290
+ response = await get_job_execution.asyncio_detailed(
291
+ job_id=self.name,
292
+ execution_id=execution_id,
293
+ client=client,
294
+ )
295
+
296
+ if response.status_code == 404:
297
+ raise Exception(f"Execution '{execution_id}' not found for job '{self.name}'")
298
+
299
+ if response.status_code != 200:
300
+ raise Exception(f"Failed to get job execution: {response.status_code}")
301
+
302
+ if not response.parsed:
303
+ raise Exception("No execution data returned")
304
+
305
+ return response.parsed
306
+
307
+ def list_executions(self, limit: int = 20, offset: int = 0) -> List[JobExecution]:
308
+ """
309
+ List all executions for this job.
310
+
311
+ Args:
312
+ limit: Maximum number of executions to return
313
+ offset: Offset for pagination
314
+
315
+ Returns:
316
+ List[JobExecution]: List of execution objects
317
+ """
318
+ logger.debug(f"Listing executions for job: {self.name}")
319
+
320
+ response = list_job_executions.sync_detailed(
321
+ job_id=self.name,
322
+ client=client,
323
+ limit=limit,
324
+ offset=offset,
325
+ )
326
+
327
+ if response.status_code != 200:
328
+ raise Exception(f"Failed to list job executions: {response.status_code}")
329
+
330
+ return response.parsed or []
331
+
332
+ async def alist_executions(self, limit: int = 20, offset: int = 0) -> List[JobExecution]:
333
+ """
334
+ List all executions for this job (async).
335
+
336
+ Args:
337
+ limit: Maximum number of executions to return
338
+ offset: Offset for pagination
339
+
340
+ Returns:
341
+ List[JobExecution]: List of execution objects
342
+ """
343
+ logger.debug(f"Listing executions for job: {self.name}")
344
+
345
+ response = await list_job_executions.asyncio_detailed(
346
+ job_id=self.name,
347
+ client=client,
348
+ limit=limit,
349
+ offset=offset,
350
+ )
351
+
352
+ if response.status_code != 200:
353
+ raise Exception(f"Failed to list job executions: {response.status_code}")
354
+
355
+ return response.parsed or []
356
+
357
+ def get_execution_status(self, execution_id: str) -> str:
358
+ """
359
+ Get the status of a specific execution.
360
+
361
+ Args:
362
+ execution_id: The execution ID
363
+
364
+ Returns:
365
+ str: The execution status
366
+ """
367
+ execution = self.get_execution(execution_id)
368
+ return execution.status if execution.status else "UNKNOWN"
369
+
370
+ async def aget_execution_status(self, execution_id: str) -> str:
371
+ """
372
+ Get the status of a specific execution (async).
373
+
374
+ Args:
375
+ execution_id: The execution ID
376
+
377
+ Returns:
378
+ str: The execution status
379
+ """
380
+ execution = await self.aget_execution(execution_id)
381
+ return execution.status if execution.status else "UNKNOWN"
382
+
383
+ def cancel_execution(self, execution_id: str) -> None:
384
+ """
385
+ Cancel a specific execution.
386
+
387
+ Args:
388
+ execution_id: The execution ID
389
+
390
+ Raises:
391
+ Exception: If the cancellation fails
392
+ """
393
+ logger.debug(f"Cancelling execution {execution_id} for job: {self.name}")
394
+
395
+ response = delete_job_execution.sync_detailed(
396
+ job_id=self.name,
397
+ execution_id=execution_id,
398
+ client=client,
399
+ )
400
+
401
+ if response.status_code != 200:
402
+ raise Exception(f"Failed to cancel job execution: {response.status_code}")
403
+
404
+ async def acancel_execution(self, execution_id: str) -> None:
405
+ """
406
+ Cancel a specific execution (async).
407
+
408
+ Args:
409
+ execution_id: The execution ID
410
+
411
+ Raises:
412
+ Exception: If the cancellation fails
413
+ """
414
+ logger.debug(f"Cancelling execution {execution_id} for job: {self.name}")
415
+
416
+ response = await delete_job_execution.asyncio_detailed(
417
+ job_id=self.name,
418
+ execution_id=execution_id,
419
+ client=client,
420
+ )
421
+
422
+ if response.status_code != 200:
423
+ raise Exception(f"Failed to cancel job execution: {response.status_code}")
424
+
425
+ def wait_for_execution(
426
+ self,
427
+ execution_id: str,
428
+ max_wait: int = 360,
429
+ interval: int = 3,
430
+ ) -> JobExecution:
431
+ """
432
+ Wait for an execution to complete.
433
+
434
+ Args:
435
+ execution_id: The execution ID to wait for
436
+ max_wait: Maximum time to wait in seconds (default: 360 = 6 minutes)
437
+ interval: Polling interval in seconds (default: 3 seconds)
438
+
439
+ Returns:
440
+ JobExecution: The completed execution
441
+
442
+ Raises:
443
+ Exception: If the execution doesn't complete within max_wait seconds
444
+ """
445
+ logger.debug(f"Waiting for execution {execution_id} to complete (max {max_wait}s)")
446
+
447
+ start_time = time.time()
448
+
449
+ while time.time() - start_time < max_wait:
450
+ execution = self.get_execution(execution_id)
451
+ status = execution.status
452
+
453
+ # Terminal states (Kubernetes-style: succeeded, failed, cancelled)
454
+ if status in ["succeeded", "failed", "cancelled"]:
455
+ logger.debug(f"Execution {execution_id} finished with status: {status}")
456
+ return execution
457
+
458
+ # Wait before polling again
459
+ time.sleep(interval)
460
+
461
+ raise Exception(f"Execution {execution_id} did not complete within {max_wait}s")
462
+
463
+ async def await_for_execution(
464
+ self,
465
+ execution_id: str,
466
+ max_wait: int = 360,
467
+ interval: int = 3,
468
+ ) -> JobExecution:
469
+ """
470
+ Wait for an execution to complete (async).
471
+
472
+ Args:
473
+ execution_id: The execution ID to wait for
474
+ max_wait: Maximum time to wait in seconds (default: 360 = 6 minutes)
475
+ interval: Polling interval in seconds (default: 3 seconds)
476
+
477
+ Returns:
478
+ JobExecution: The completed execution
479
+
480
+ Raises:
481
+ Exception: If the execution doesn't complete within max_wait seconds
482
+ """
483
+ logger.debug(f"Waiting for execution {execution_id} to complete (max {max_wait}s)")
484
+
485
+ start_time = time.time()
486
+
487
+ while time.time() - start_time < max_wait:
488
+ execution = await self.aget_execution(execution_id)
489
+ status = execution.status
490
+
491
+ # Terminal states (Kubernetes-style: succeeded, failed, cancelled)
492
+ if status in ["succeeded", "failed", "cancelled"]:
493
+ logger.debug(f"Execution {execution_id} finished with status: {status}")
494
+ return execution
495
+
496
+ # Wait before polling again
497
+ await asyncio.sleep(interval)
498
+
499
+ raise Exception(f"Execution {execution_id} did not complete within {max_wait}s")
500
+
154
501
  def __str__(self):
155
502
  return f"Job {self.name}"
156
503
 
@@ -2,8 +2,6 @@
2
2
 
3
3
  from .apply_edit_request import ApplyEditRequest
4
4
  from .apply_edit_response import ApplyEditResponse
5
- from .content_search_match import ContentSearchMatch
6
- from .content_search_response import ContentSearchResponse
7
5
  from .delete_network_process_pid_monitor_response_200 import (
8
6
  DeleteNetworkProcessPidMonitorResponse200,
9
7
  )
@@ -15,10 +13,6 @@ from .file_with_content import FileWithContent
15
13
  from .filesystem_multipart_upload import FilesystemMultipartUpload
16
14
  from .filesystem_multipart_upload_parts import FilesystemMultipartUploadParts
17
15
  from .filesystem_uploaded_part import FilesystemUploadedPart
18
- from .find_match import FindMatch
19
- from .find_response import FindResponse
20
- from .fuzzy_search_match import FuzzySearchMatch
21
- from .fuzzy_search_response import FuzzySearchResponse
22
16
  from .get_network_process_pid_ports_response_200 import GetNetworkProcessPidPortsResponse200
23
17
  from .multipart_complete_request import MultipartCompleteRequest
24
18
  from .multipart_initiate_request import MultipartInitiateRequest
@@ -39,15 +33,11 @@ from .ranked_file import RankedFile
39
33
  from .reranking_response import RerankingResponse
40
34
  from .subdirectory import Subdirectory
41
35
  from .success_response import SuccessResponse
42
- from .tree_request import TreeRequest
43
- from .tree_request_files import TreeRequestFiles
44
36
  from .welcome_response import WelcomeResponse
45
37
 
46
38
  __all__ = (
47
39
  "ApplyEditRequest",
48
40
  "ApplyEditResponse",
49
- "ContentSearchMatch",
50
- "ContentSearchResponse",
51
41
  "DeleteNetworkProcessPidMonitorResponse200",
52
42
  "Directory",
53
43
  "ErrorResponse",
@@ -57,10 +47,6 @@ __all__ = (
57
47
  "FilesystemMultipartUploadParts",
58
48
  "FilesystemUploadedPart",
59
49
  "FileWithContent",
60
- "FindMatch",
61
- "FindResponse",
62
- "FuzzySearchMatch",
63
- "FuzzySearchResponse",
64
50
  "GetNetworkProcessPidPortsResponse200",
65
51
  "MultipartCompleteRequest",
66
52
  "MultipartInitiateRequest",
@@ -81,7 +67,5 @@ __all__ = (
81
67
  "RerankingResponse",
82
68
  "Subdirectory",
83
69
  "SuccessResponse",
84
- "TreeRequest",
85
- "TreeRequestFiles",
86
70
  "WelcomeResponse",
87
71
  )