fastsdk 0.2.30__tar.gz → 0.2.32__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.
- {fastsdk-0.2.30 → fastsdk-0.2.32}/PKG-INFO +1 -1
- fastsdk-0.2.32/fastsdk/service_interaction/__init__.py +16 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/service_interaction/api_job_manager.py +137 -103
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/service_interaction/api_seex.py +53 -43
- fastsdk-0.2.32/fastsdk/service_interaction/request/__init__.py +14 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/service_interaction/request/api_client.py +46 -84
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/service_interaction/request/api_client_replicate.py +18 -6
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/service_interaction/request/api_client_runpod.py +52 -10
- fastsdk-0.2.32/fastsdk/service_interaction/request/api_client_socaity.py +104 -0
- fastsdk-0.2.32/fastsdk/service_interaction/response/response_parser.py +207 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/service_interaction/response/response_parser_strategies.py +1 -1
- fastsdk-0.2.32/fastsdk/service_interaction/response/response_schemas.py +141 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk.egg-info/PKG-INFO +1 -1
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk.egg-info/SOURCES.txt +1 -1
- {fastsdk-0.2.30 → fastsdk-0.2.32}/pyproject.toml +1 -1
- fastsdk-0.2.30/fastsdk/service_interaction/__init__.py +0 -5
- fastsdk-0.2.30/fastsdk/service_interaction/request/__init__.py +0 -7
- fastsdk-0.2.30/fastsdk/service_interaction/request/api_client_socaity.py +0 -41
- fastsdk-0.2.30/fastsdk/service_interaction/response/base_response.py +0 -77
- fastsdk-0.2.30/fastsdk/service_interaction/response/response_parser.py +0 -106
- {fastsdk-0.2.30 → fastsdk-0.2.32}/LICENSE +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/README.md +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/__init__.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/fastClient.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/fastSDK.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/sdk_factory/__init__.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/sdk_factory/sdk_factory.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/sdk_factory/sdk_template.j2 +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/service_interaction/request/file_handler.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/service_interaction/response/api_job_status.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/service_specification_loader/runpod_open_api_loader.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk/service_specification_loader/spec_loader.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk.egg-info/dependency_links.txt +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk.egg-info/requires.txt +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/fastsdk.egg-info/top_level.txt +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/setup.cfg +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/test/test_client_factory.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/test/test_client_from_fastapi.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/test/test_client_from_runpod_serverless.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/test/test_httpx_api.py +0 -0
- {fastsdk-0.2.30 → fastsdk-0.2.32}/test/test_manual_sdk.py +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .api_seex import APISeex
|
|
2
|
+
from .response.response_schemas import (
|
|
3
|
+
SocaityJobResponse, RunpodJobResponse, ReplicateJobResponse,
|
|
4
|
+
StreamingResponse, JOB_RESPONSE_TYPES,
|
|
5
|
+
)
|
|
6
|
+
from .api_job_manager import ApiJobManager
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"SocaityJobResponse",
|
|
10
|
+
"RunpodJobResponse",
|
|
11
|
+
"ReplicateJobResponse",
|
|
12
|
+
"StreamingResponse",
|
|
13
|
+
"JOB_RESPONSE_TYPES",
|
|
14
|
+
"APISeex",
|
|
15
|
+
"ApiJobManager",
|
|
16
|
+
]
|
|
@@ -4,7 +4,10 @@ from typing import Any, Dict, Optional
|
|
|
4
4
|
|
|
5
5
|
logger = logging.getLogger(__name__)
|
|
6
6
|
|
|
7
|
-
from apipod_registry.definitions.service_definitions import
|
|
7
|
+
from apipod_registry.definitions.service_definitions import (
|
|
8
|
+
ServiceDefinition, ServiceAddress,
|
|
9
|
+
RunpodServiceAddress, ReplicateServiceAddress, SocaityServiceAddress,
|
|
10
|
+
)
|
|
8
11
|
from apipod_registry.registry import Registry
|
|
9
12
|
|
|
10
13
|
from fastsdk.service_interaction.api_seex import APISeex
|
|
@@ -15,64 +18,92 @@ from fastsdk.service_interaction.request.file_handler import FileHandler
|
|
|
15
18
|
from fastCloud import ReplicateUploadAPI
|
|
16
19
|
|
|
17
20
|
from fastsdk.service_interaction.response.response_parser import ResponseParser
|
|
18
|
-
from fastsdk.service_interaction.response.
|
|
21
|
+
from fastsdk.service_interaction.response.response_schemas import JOB_RESPONSE_TYPES, StreamingResponse
|
|
19
22
|
|
|
20
|
-
from fastsdk.service_interaction.request import
|
|
23
|
+
from fastsdk.service_interaction.request import (
|
|
24
|
+
APIClient, APIClientReplicate, APIClientRunpod, APIClientSocaity, RequestData,
|
|
25
|
+
)
|
|
26
|
+
from fastsdk.service_interaction.request.api_client_runpod import APIClientRunpodApipod
|
|
21
27
|
from fastsdk.service_interaction.response.api_job_status import APIJobStatus
|
|
22
28
|
from media_toolkit import MediaDict
|
|
23
29
|
|
|
30
|
+
|
|
24
31
|
class ApiJobManager:
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
"""Manages the lifecycle of asynchronous API jobs by orchestrating services."""
|
|
33
|
+
|
|
34
|
+
_CLIENT_CLASSES = {
|
|
35
|
+
"runpod": APIClientRunpod,
|
|
36
|
+
"runpod_apipod": APIClientRunpodApipod,
|
|
37
|
+
"socaity": APIClientSocaity,
|
|
38
|
+
"replicate": APIClientReplicate,
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
def __init__(self, service_registry: Registry, progress_verbosity: int = 2):
|
|
30
42
|
self.service_registry = service_registry
|
|
31
43
|
self.api_clients: Dict[str, APIClient] = {}
|
|
32
44
|
self.file_handlers: Dict[str, FileHandler] = {}
|
|
33
|
-
self.
|
|
45
|
+
self._provider_types: Dict[str, str] = {}
|
|
46
|
+
self._parser_cache: Dict[str, ResponseParser] = {}
|
|
34
47
|
self.tasks = {
|
|
35
48
|
"Preparing": self._prepare_request,
|
|
36
49
|
"Load files": self._load_files,
|
|
37
50
|
"Uploading files": self._upload_files,
|
|
38
51
|
"Sending request": self._send_request,
|
|
39
52
|
"Polling": self._poll_status,
|
|
40
|
-
"Processing result": self._process_result
|
|
53
|
+
"Processing result": self._process_result,
|
|
41
54
|
}
|
|
42
55
|
self.meseex_box = MeseexBox(task_methods=self.tasks, progress_verbosity=progress_verbosity)
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# Provider resolution & parser cache
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def _determine_service_type(service_def: ServiceDefinition) -> str:
|
|
63
|
+
addr = service_def.service_address
|
|
64
|
+
if isinstance(addr, RunpodServiceAddress):
|
|
65
|
+
if service_def.specification in ("apipod", "socaity"):
|
|
66
|
+
return "runpod_apipod"
|
|
46
67
|
return "runpod"
|
|
47
|
-
|
|
68
|
+
if isinstance(addr, SocaityServiceAddress):
|
|
48
69
|
return "socaity"
|
|
49
|
-
|
|
70
|
+
if isinstance(addr, ReplicateServiceAddress):
|
|
50
71
|
return "replicate"
|
|
51
|
-
|
|
72
|
+
if isinstance(addr, ServiceAddress):
|
|
52
73
|
if service_def.specification in ("apipod", "socaity"):
|
|
53
74
|
return "socaity"
|
|
54
|
-
|
|
75
|
+
if service_def.specification == "runpod":
|
|
55
76
|
return "runpod"
|
|
56
|
-
|
|
57
77
|
return "other"
|
|
58
78
|
|
|
79
|
+
def _get_parser(self, service_id: str) -> ResponseParser:
|
|
80
|
+
provider = self._provider_types.get(service_id, "other")
|
|
81
|
+
if provider not in self._parser_cache:
|
|
82
|
+
self._parser_cache[provider] = ResponseParser(provider)
|
|
83
|
+
return self._parser_cache[provider]
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
# Client / handler registration
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
|
|
59
89
|
def add_api_client(self, service_id: str, api_key: str):
|
|
60
|
-
if service_id
|
|
61
|
-
|
|
62
|
-
if not service_def:
|
|
63
|
-
raise ValueError(f"Service {service_id} not found")
|
|
90
|
+
if service_id in self.api_clients:
|
|
91
|
+
return
|
|
64
92
|
|
|
65
|
-
|
|
66
|
-
|
|
93
|
+
service_def = self.service_registry.get_service(service_id)
|
|
94
|
+
if not service_def:
|
|
95
|
+
raise ValueError(f"Service {service_id} not found")
|
|
96
|
+
if not hasattr(service_def, "service_address") or service_def.service_address is None:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"Service {service_id} has no service address. "
|
|
99
|
+
"Add one with Registry.update_service(service_id, service_address=...)"
|
|
100
|
+
)
|
|
67
101
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"runpod": APIClientRunpod,
|
|
71
|
-
"socaity": APIClientSocaity,
|
|
72
|
-
"replicate": APIClientReplicate,
|
|
73
|
-
}.get(service_type, APIClient)
|
|
102
|
+
service_type = self._determine_service_type(service_def)
|
|
103
|
+
self._provider_types[service_id] = service_type
|
|
74
104
|
|
|
75
|
-
|
|
105
|
+
client_cls = self._CLIENT_CLASSES.get(service_type, APIClient)
|
|
106
|
+
self.api_clients[service_id] = client_cls(service_def=service_def, api_key=api_key)
|
|
76
107
|
|
|
77
108
|
def add_file_handler(self, service_id: str, api_key: str = None, file_handler: FileHandler = None):
|
|
78
109
|
if file_handler is not None:
|
|
@@ -84,7 +115,7 @@ class ApiJobManager:
|
|
|
84
115
|
|
|
85
116
|
if service_type == "socaity":
|
|
86
117
|
file_handler = FileHandler(file_format="httpx", upload_to_cloud_threshold_mb=0, max_upload_file_size_mb=300)
|
|
87
|
-
elif service_type
|
|
118
|
+
elif service_type in ("runpod", "runpod_apipod"):
|
|
88
119
|
file_handler = FileHandler(file_format="base64", max_upload_file_size_mb=300)
|
|
89
120
|
elif service_type == "replicate":
|
|
90
121
|
fast_cloud = ReplicateUploadAPI(api_key=api_key)
|
|
@@ -103,6 +134,10 @@ class ApiJobManager:
|
|
|
103
134
|
self.add_file_handler(service_def.id, api_key)
|
|
104
135
|
return service_def
|
|
105
136
|
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
# Task implementations
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
|
|
106
141
|
async def _prepare_request(self, job: APISeex) -> RequestData:
|
|
107
142
|
api_client = self.api_clients[job.service_def.id]
|
|
108
143
|
return api_client.format_request_params(job.endpoint_def, job.input)
|
|
@@ -111,7 +146,6 @@ class ApiJobManager:
|
|
|
111
146
|
request_data = job.prev_task_output
|
|
112
147
|
if not request_data.file_params:
|
|
113
148
|
return request_data
|
|
114
|
-
|
|
115
149
|
fh = self.file_handlers.get(job.service_def.id)
|
|
116
150
|
request_data.file_params = await fh.load_files_from_disk(request_data.file_params)
|
|
117
151
|
return request_data
|
|
@@ -120,7 +154,6 @@ class ApiJobManager:
|
|
|
120
154
|
request_data = job.prev_task_output
|
|
121
155
|
if not request_data.file_params:
|
|
122
156
|
return request_data
|
|
123
|
-
|
|
124
157
|
fh = self.file_handlers.get(job.service_def.id)
|
|
125
158
|
request_data.file_params = await fh.upload_files(request_data.file_params)
|
|
126
159
|
return request_data
|
|
@@ -128,6 +161,7 @@ class ApiJobManager:
|
|
|
128
161
|
async def _send_request(self, job: APISeex) -> Any:
|
|
129
162
|
request_data = job.prev_task_output
|
|
130
163
|
api_client = self.api_clients[job.service_def.id]
|
|
164
|
+
parser = self._get_parser(job.service_def.id)
|
|
131
165
|
|
|
132
166
|
if isinstance(request_data.file_params, MediaDict):
|
|
133
167
|
non_file_params = request_data.file_params.get_non_file_params(include_urls=True)
|
|
@@ -136,26 +170,25 @@ class ApiJobManager:
|
|
|
136
170
|
|
|
137
171
|
fh = self.file_handlers.get(job.service_def.id)
|
|
138
172
|
request_data.file_params = await fh.prepare_files_for_send(request_data.file_params)
|
|
139
|
-
|
|
173
|
+
|
|
140
174
|
logger.info("_send_request | Sending request to %s", request_data.url)
|
|
141
175
|
response = await api_client.send_request(request_data)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
176
|
+
logger.info(
|
|
177
|
+
"_send_request | Received response: status=%d content_type=%s",
|
|
178
|
+
response.status_code, response.headers.get("Content-Type"),
|
|
179
|
+
)
|
|
145
180
|
|
|
146
|
-
error = await
|
|
181
|
+
error = await parser.check_response_status(response)
|
|
147
182
|
if error:
|
|
148
183
|
logger.error("_send_request | Request failed: %s", error)
|
|
149
184
|
raise Exception(error)
|
|
150
185
|
|
|
151
|
-
parsed = await
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if isinstance(parsed, BaseJobResponse) and parsed.status == APIJobStatus.STREAMING:
|
|
186
|
+
parsed = await parser.parse_response(response)
|
|
187
|
+
|
|
188
|
+
if isinstance(parsed, StreamingResponse):
|
|
155
189
|
logger.info("_send_request | Detected direct stream response")
|
|
156
190
|
job.direct_response = response
|
|
157
191
|
else:
|
|
158
|
-
# If not streaming, ensure response is closed after reading
|
|
159
192
|
if not response.is_closed:
|
|
160
193
|
await response.aclose()
|
|
161
194
|
|
|
@@ -166,78 +199,76 @@ class ApiJobManager:
|
|
|
166
199
|
async def _poll_status(self, job: APISeex) -> Any:
|
|
167
200
|
parsed_response = job.prev_task_output
|
|
168
201
|
|
|
169
|
-
if not isinstance(parsed_response,
|
|
170
|
-
return parsed_response
|
|
171
|
-
|
|
172
|
-
# Streaming responses are terminal from the SDK's perspective.
|
|
173
|
-
# The worker's override handles forwarding chunks to Redis.
|
|
174
|
-
if parsed_response.status == APIJobStatus.STREAMING:
|
|
202
|
+
if not isinstance(parsed_response, JOB_RESPONSE_TYPES):
|
|
175
203
|
return parsed_response
|
|
176
204
|
|
|
177
205
|
api_client = self.api_clients[job.service_def.id]
|
|
206
|
+
parser = self._get_parser(job.service_def.id)
|
|
178
207
|
|
|
179
208
|
try:
|
|
180
209
|
http_response = await api_client.poll_status(parsed_response)
|
|
181
210
|
except Exception as e:
|
|
182
211
|
n_errors = job.get_task_data() or {}
|
|
183
212
|
n_polling_errors = n_errors.get("number_of_polling_errors", 0) if isinstance(n_errors, dict) else 0
|
|
184
|
-
|
|
185
213
|
if n_polling_errors > 3:
|
|
186
214
|
raise e
|
|
187
215
|
job.set_task_data({"number_of_polling_errors": n_polling_errors + 1})
|
|
188
216
|
return PollAgain(f"Job status polling failed: {e}")
|
|
189
217
|
|
|
190
|
-
error = await
|
|
218
|
+
error = await parser.check_response_status(http_response)
|
|
191
219
|
if error:
|
|
192
220
|
if not http_response.is_closed:
|
|
193
221
|
await http_response.aclose()
|
|
194
222
|
raise ValueError(f"Job status polling failed: {error}")
|
|
195
223
|
|
|
196
|
-
parsed_response = await
|
|
197
|
-
|
|
198
|
-
# Ensure response is closed after reading
|
|
224
|
+
parsed_response = await parser.parse_response(http_response, parse_media=False)
|
|
225
|
+
|
|
199
226
|
if not http_response.is_closed:
|
|
200
227
|
await http_response.aclose()
|
|
201
228
|
|
|
202
|
-
if not isinstance(parsed_response,
|
|
229
|
+
if not isinstance(parsed_response, JOB_RESPONSE_TYPES):
|
|
203
230
|
raise ValueError(f"Expected job response but got {type(parsed_response)}")
|
|
204
231
|
|
|
205
|
-
|
|
232
|
+
status = api_client.get_status(parsed_response)
|
|
233
|
+
|
|
234
|
+
if status == APIJobStatus.FINISHED:
|
|
206
235
|
return parsed_response
|
|
207
|
-
|
|
236
|
+
if status == APIJobStatus.CANCELLED:
|
|
208
237
|
job.mark_cancelled(cancel_result=parsed_response)
|
|
209
238
|
return parsed_response
|
|
210
|
-
|
|
211
|
-
|
|
239
|
+
if status == APIJobStatus.FAILED:
|
|
240
|
+
err = getattr(parsed_response, "error", None)
|
|
241
|
+
raise ValueError(err or f"Job failed with status: {getattr(parsed_response, 'status', 'unknown')}")
|
|
212
242
|
|
|
213
|
-
|
|
243
|
+
progress = getattr(parsed_response, "progress", None)
|
|
214
244
|
message = getattr(parsed_response, "message", None)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
245
|
+
raw_status = getattr(parsed_response, "status", "unknown")
|
|
246
|
+
|
|
247
|
+
progress_msg = f"Job {getattr(parsed_response, 'id', getattr(parsed_response, 'job_id', '?'))}"
|
|
248
|
+
progress_msg += f": {message}" if message else f" status: {raw_status}"
|
|
219
249
|
|
|
220
|
-
job.set_task_progress(
|
|
250
|
+
job.set_task_progress(progress, progress_msg)
|
|
221
251
|
job.set_task_output(parsed_response)
|
|
222
|
-
return PollAgain(f"Job status: {
|
|
252
|
+
return PollAgain(f"Job status: {raw_status}")
|
|
223
253
|
|
|
224
254
|
async def _process_result(self, job: APISeex) -> Any:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
255
|
+
response = job.prev_task_output
|
|
256
|
+
|
|
257
|
+
if isinstance(response, StreamingResponse):
|
|
258
|
+
return response
|
|
228
259
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return result
|
|
260
|
+
if not isinstance(response, JOB_RESPONSE_TYPES):
|
|
261
|
+
return response
|
|
262
|
+
|
|
263
|
+
api_client = self.api_clients[job.service_def.id]
|
|
264
|
+
parser = self._get_parser(job.service_def.id)
|
|
235
265
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return result
|
|
266
|
+
raw_result = api_client.get_result(response)
|
|
267
|
+
return parser.parse_media(raw_result)
|
|
239
268
|
|
|
240
|
-
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
# Job submission
|
|
271
|
+
# ------------------------------------------------------------------
|
|
241
272
|
|
|
242
273
|
def submit_job(self, service_id: str, endpoint_id: str, data: dict) -> APISeex:
|
|
243
274
|
service_def = self.service_registry.get_service(service_id)
|
|
@@ -276,60 +307,63 @@ class ApiJobManager:
|
|
|
276
307
|
data=data,
|
|
277
308
|
tasks=task_list,
|
|
278
309
|
name=seex_name,
|
|
279
|
-
cancel_handler=self.cancel_api_job
|
|
310
|
+
cancel_handler=self.cancel_api_job,
|
|
280
311
|
)
|
|
312
|
+
job._meseex_box = self.meseex_box
|
|
313
|
+
job._api_client = self.api_clients[service_id]
|
|
314
|
+
job._response_parser = self._get_parser(service_id)
|
|
281
315
|
|
|
282
316
|
return self.meseex_box.summon_meseex(job)
|
|
283
317
|
|
|
318
|
+
# ------------------------------------------------------------------
|
|
319
|
+
# Cancellation
|
|
320
|
+
# ------------------------------------------------------------------
|
|
321
|
+
|
|
284
322
|
def _run_async_call(self, method, *args, timeout_s: float = 30.0):
|
|
285
323
|
"""Bridge helper: run an async method synchronously via the task executor."""
|
|
286
324
|
task = self.meseex_box.task_executor.submit(method, *args)
|
|
287
325
|
started_at = time.monotonic()
|
|
288
|
-
|
|
289
326
|
while not task.is_completed:
|
|
290
327
|
if timeout_s is not None and (time.monotonic() - started_at) > timeout_s:
|
|
291
328
|
task.cancel()
|
|
292
329
|
raise TimeoutError("Timed out while waiting for async call")
|
|
293
330
|
time.sleep(0.01)
|
|
294
|
-
|
|
295
331
|
if task.error is not None:
|
|
296
332
|
raise task.error
|
|
297
|
-
|
|
298
333
|
return task.result
|
|
299
334
|
|
|
300
|
-
def _try_remote_cancel(self, job: APISeex)
|
|
335
|
+
def _try_remote_cancel(self, job: APISeex):
|
|
301
336
|
"""Best-effort: send a cancel request to the remote API."""
|
|
302
337
|
current_response = job.response
|
|
303
|
-
|
|
338
|
+
api_client = self.api_clients[job.service_def.id]
|
|
339
|
+
|
|
340
|
+
if not isinstance(current_response, JOB_RESPONSE_TYPES) or not api_client.get_cancel_url(current_response):
|
|
304
341
|
return None
|
|
305
342
|
|
|
306
|
-
http_response = self._run_async_call(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
310
|
-
error = self._run_async_call(self.response_parser.check_response_status, http_response)
|
|
343
|
+
http_response = self._run_async_call(api_client.cancel_job, current_response)
|
|
344
|
+
parser = self._get_parser(job.service_def.id)
|
|
345
|
+
|
|
346
|
+
error = self._run_async_call(parser.check_response_status, http_response)
|
|
311
347
|
if error:
|
|
312
|
-
parsed_error = self._run_async_call(
|
|
348
|
+
parsed_error = self._run_async_call(parser.parse_response, http_response, False)
|
|
313
349
|
if not http_response.is_closed:
|
|
314
350
|
self._run_async_call(http_response.aclose)
|
|
315
|
-
if isinstance(parsed_error,
|
|
351
|
+
if isinstance(parsed_error, JOB_RESPONSE_TYPES):
|
|
316
352
|
return parsed_error
|
|
317
353
|
raise ValueError(f"Remote cancellation failed with HTTP error: {error}")
|
|
318
354
|
|
|
319
|
-
parsed = self._run_async_call(
|
|
355
|
+
parsed = self._run_async_call(parser.parse_response, http_response, False)
|
|
320
356
|
if not http_response.is_closed:
|
|
321
357
|
self._run_async_call(http_response.aclose)
|
|
322
|
-
return parsed if isinstance(parsed,
|
|
358
|
+
return parsed if isinstance(parsed, JOB_RESPONSE_TYPES) else None
|
|
323
359
|
|
|
324
360
|
def cancel_api_job(self, job: APISeex, **kwargs) -> Any:
|
|
325
361
|
"""Best-effort cancel: send remote cancel request if possible."""
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
service_specification=job.service_def.specification,
|
|
332
|
-
)
|
|
362
|
+
api_client = self.api_clients.get(job.service_def.id)
|
|
363
|
+
current = job.response
|
|
364
|
+
|
|
365
|
+
if not isinstance(current, JOB_RESPONSE_TYPES) or not api_client or not api_client.get_cancel_url(current):
|
|
366
|
+
local_cancel = {"id": job.meseex_id, "status": "CANCELLED", "error": "Cancelled before remote submission"}
|
|
333
367
|
self.meseex_box.cancel_meseex(job, cancel_result=local_cancel)
|
|
334
368
|
return local_cancel
|
|
335
369
|
|
|
@@ -337,13 +371,13 @@ class ApiJobManager:
|
|
|
337
371
|
remote_response = self._try_remote_cancel(job)
|
|
338
372
|
except Exception as e:
|
|
339
373
|
print(f"Warning: Remote cancellation for job {job.meseex_id} failed: {e}. Job will continue polling.")
|
|
340
|
-
return
|
|
374
|
+
return current
|
|
341
375
|
|
|
342
376
|
if remote_response is None:
|
|
343
377
|
print(f"Warning: Job {job.meseex_id} has no remote cancel URL or no job response. Job will continue polling.")
|
|
344
|
-
return
|
|
378
|
+
return current
|
|
345
379
|
|
|
346
|
-
if remote_response
|
|
380
|
+
if api_client.get_status(remote_response) == APIJobStatus.CANCELLED:
|
|
347
381
|
self.meseex_box.cancel_meseex(job, cancel_result=remote_response)
|
|
348
382
|
return remote_response
|
|
349
383
|
|