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.
- blaxel/__init__.py +2 -2
- blaxel/core/__init__.py +9 -1
- blaxel/core/client/api/{privateclusters/create_private_cluster.py → images/cleanup_images.py} +31 -25
- blaxel/core/client/models/__init__.py +2 -4
- blaxel/core/client/models/agent.py +1 -0
- blaxel/core/client/models/agent_spec.py +1 -24
- blaxel/core/client/models/billable_time_metric.py +1 -0
- blaxel/core/{sandbox/client/models/find_match.py → client/models/cleanup_images_response_200.py} +19 -19
- blaxel/core/client/models/configuration.py +1 -0
- blaxel/core/client/models/core_event.py +9 -0
- blaxel/core/client/models/core_spec.py +1 -24
- blaxel/core/client/models/core_spec_configurations.py +1 -0
- blaxel/core/client/models/create_job_execution_request.py +1 -0
- blaxel/core/client/models/create_job_execution_response.py +1 -0
- blaxel/core/client/models/custom_domain.py +1 -0
- blaxel/core/client/models/custom_domain_metadata.py +1 -0
- blaxel/core/client/models/custom_domain_spec.py +1 -0
- blaxel/core/client/models/delete_volume_template_version_response_200.py +1 -0
- blaxel/core/client/models/entrypoint.py +1 -0
- blaxel/core/client/models/form.py +1 -0
- blaxel/core/client/models/function.py +1 -0
- blaxel/core/client/models/function_spec.py +1 -24
- blaxel/core/client/models/image.py +1 -0
- blaxel/core/client/models/image_spec.py +1 -0
- blaxel/core/client/models/integration.py +1 -0
- blaxel/core/client/models/integration_connection.py +1 -0
- blaxel/core/client/models/integration_connection_spec.py +1 -0
- blaxel/core/client/models/integration_endpoint.py +1 -0
- blaxel/core/client/models/integration_endpoints.py +2 -0
- blaxel/core/client/models/job.py +1 -0
- blaxel/core/client/models/job_execution.py +1 -0
- blaxel/core/client/models/job_execution_spec.py +1 -0
- blaxel/core/client/models/job_execution_task.py +1 -0
- blaxel/core/client/models/job_metrics.py +1 -0
- blaxel/core/client/models/job_spec.py +1 -24
- blaxel/core/client/models/jobs_network_chart.py +1 -0
- blaxel/core/client/models/jobs_success_failed_chart.py +1 -0
- blaxel/core/client/models/latency_metric.py +1 -0
- blaxel/core/client/models/location_response.py +1 -0
- blaxel/core/client/models/mcp_definition.py +1 -0
- blaxel/core/client/models/metadata.py +1 -0
- blaxel/core/client/models/metrics.py +34 -0
- blaxel/core/client/models/model.py +1 -0
- blaxel/core/client/models/model_spec.py +1 -24
- blaxel/core/client/models/pending_invitation_accept.py +1 -0
- blaxel/core/client/models/pending_invitation_render.py +1 -0
- blaxel/core/client/models/policy.py +1 -0
- blaxel/core/client/models/policy_spec.py +1 -0
- blaxel/core/client/models/preview.py +1 -0
- blaxel/core/client/models/preview_spec.py +1 -0
- blaxel/core/client/models/preview_token.py +1 -0
- blaxel/core/client/models/public_ips.py +1 -0
- blaxel/core/client/models/request_duration_over_time_metrics.py +1 -0
- blaxel/core/client/models/request_total_by_origin_metric.py +1 -0
- blaxel/core/client/models/request_total_metric.py +1 -0
- blaxel/core/client/models/resource_metrics.py +1 -0
- blaxel/core/client/models/revision_configuration.py +9 -0
- blaxel/core/client/models/runtime.py +1 -0
- blaxel/core/client/models/sandbox.py +1 -0
- blaxel/core/client/models/sandbox_definition.py +1 -0
- blaxel/core/client/models/sandbox_lifecycle.py +1 -0
- blaxel/core/client/models/sandbox_spec.py +1 -24
- blaxel/core/client/models/serverless_config.py +1 -0
- blaxel/core/client/models/start_sandbox.py +1 -0
- blaxel/core/client/models/stop_sandbox.py +1 -0
- blaxel/core/client/models/store_agent.py +1 -0
- blaxel/core/client/models/store_configuration.py +1 -0
- blaxel/core/client/models/template.py +1 -0
- blaxel/core/client/models/time_to_first_token_over_time_metrics.py +1 -0
- blaxel/core/client/models/token_rate_metrics.py +1 -0
- blaxel/core/client/models/trigger.py +10 -0
- blaxel/core/client/models/trigger_configuration.py +28 -0
- blaxel/core/client/models/volume.py +1 -0
- blaxel/core/client/models/volume_template.py +1 -0
- blaxel/core/client/models/websocket_channel.py +9 -0
- blaxel/core/client/models/workspace.py +1 -0
- blaxel/core/client/response_interceptor.py +0 -1
- blaxel/core/common/__init__.py +10 -0
- blaxel/core/common/autoload.py +5 -4
- blaxel/core/common/settings.py +29 -0
- blaxel/core/common/webhook.py +187 -0
- blaxel/core/jobs/__init__.py +355 -8
- blaxel/core/sandbox/client/models/__init__.py +0 -16
- blaxel/core/sandbox/default/action.py +10 -27
- blaxel/core/sandbox/default/filesystem.py +52 -187
- blaxel/core/sandbox/default/interpreter.py +55 -62
- blaxel/core/sandbox/default/process.py +46 -66
- blaxel/core/sandbox/sync/filesystem.py +5 -2
- blaxel/googleadk/model.py +0 -1
- blaxel/livekit/model.py +0 -1
- {blaxel-0.2.26rc119.dist-info → blaxel-0.2.28.dist-info}/METADATA +2 -2
- {blaxel-0.2.26rc119.dist-info → blaxel-0.2.28.dist-info}/RECORD +94 -117
- blaxel/core/client/api/privateclusters/__init__.py +0 -0
- blaxel/core/client/api/privateclusters/delete_private_cluster.py +0 -152
- blaxel/core/client/api/privateclusters/get_private_cluster.py +0 -155
- blaxel/core/client/api/privateclusters/get_private_cluster_health.py +0 -97
- blaxel/core/client/api/privateclusters/list_private_clusters.py +0 -136
- blaxel/core/client/api/privateclusters/update_private_cluster.py +0 -152
- blaxel/core/client/api/privateclusters/update_private_cluster_health.py +0 -97
- blaxel/core/client/models/model_private_cluster.py +0 -79
- blaxel/core/client/models/private_cluster.py +0 -183
- blaxel/core/sandbox/client/api/filesystem/delete_filesystem_tree_path.py +0 -188
- blaxel/core/sandbox/client/api/filesystem/get_filesystem_content_search_path.py +0 -265
- blaxel/core/sandbox/client/api/filesystem/get_filesystem_find_path.py +0 -248
- blaxel/core/sandbox/client/api/filesystem/get_filesystem_search_path.py +0 -237
- blaxel/core/sandbox/client/api/filesystem/get_filesystem_tree_path.py +0 -197
- blaxel/core/sandbox/client/api/filesystem/put_filesystem_tree_path.py +0 -223
- blaxel/core/sandbox/client/api/websocket/__init__.py +0 -0
- blaxel/core/sandbox/client/api/websocket/get_ws.py +0 -81
- blaxel/core/sandbox/client/models/content_search_match.py +0 -98
- blaxel/core/sandbox/client/models/content_search_response.py +0 -97
- blaxel/core/sandbox/client/models/find_response.py +0 -88
- blaxel/core/sandbox/client/models/fuzzy_search_match.py +0 -78
- blaxel/core/sandbox/client/models/fuzzy_search_response.py +0 -88
- blaxel/core/sandbox/client/models/tree_request.py +0 -76
- blaxel/core/sandbox/client/models/tree_request_files.py +0 -49
- {blaxel-0.2.26rc119.dist-info → blaxel-0.2.28.dist-info}/WHEEL +0 -0
- {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
|
+
|
blaxel/core/jobs/__init__.py
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import asyncio
|
|
3
3
|
import os
|
|
4
|
-
import
|
|
4
|
+
import time
|
|
5
5
|
from logging import getLogger
|
|
6
|
-
from typing import Any,
|
|
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
|
-
|
|
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 = {}) ->
|
|
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
|
)
|