poetry-plugin-ivcap 0.5.0__py3-none-any.whl → 0.5.1__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.
- poetry_plugin_ivcap/ivcap.py +95 -29
- {poetry_plugin_ivcap-0.5.0.dist-info → poetry_plugin_ivcap-0.5.1.dist-info}/METADATA +1 -1
- {poetry_plugin_ivcap-0.5.0.dist-info → poetry_plugin_ivcap-0.5.1.dist-info}/RECORD +7 -7
- {poetry_plugin_ivcap-0.5.0.dist-info → poetry_plugin_ivcap-0.5.1.dist-info}/AUTHORS.md +0 -0
- {poetry_plugin_ivcap-0.5.0.dist-info → poetry_plugin_ivcap-0.5.1.dist-info}/LICENSE +0 -0
- {poetry_plugin_ivcap-0.5.0.dist-info → poetry_plugin_ivcap-0.5.1.dist-info}/WHEEL +0 -0
- {poetry_plugin_ivcap-0.5.0.dist-info → poetry_plugin_ivcap-0.5.1.dist-info}/entry_points.txt +0 -0
poetry_plugin_ivcap/ivcap.py
CHANGED
|
@@ -158,13 +158,15 @@ def exec_job(data, args, is_silent, line):
|
|
|
158
158
|
pa = p.parse_args(args)
|
|
159
159
|
timeout = 0 if pa.stream else pa.timeout
|
|
160
160
|
# Get access token using ivcap CLI
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
161
|
+
token = pa.auth_token
|
|
162
|
+
if not token:
|
|
163
|
+
try:
|
|
164
|
+
token = subprocess.check_output(
|
|
165
|
+
["ivcap", "--silent", "context", "get", "access-token", "--refresh-token"],
|
|
166
|
+
text=True
|
|
167
|
+
).strip()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
raise RuntimeError(f"Failed to get IVCAP access token: {e}")
|
|
168
170
|
|
|
169
171
|
# Get IVCAP deployment URL
|
|
170
172
|
try:
|
|
@@ -211,15 +213,14 @@ def exec_job(data, args, is_silent, line):
|
|
|
211
213
|
location = f"{payload.get('location')}"
|
|
212
214
|
job_id = payload.get("job-id")
|
|
213
215
|
retry_later = payload.get("retry-later", 5)
|
|
214
|
-
if not is_silent:
|
|
215
|
-
line(f"<debug>Job '{job_id}' accepted, but no result yet. Polling in {retry_later} seconds.</debug>")
|
|
216
|
-
if pa.stream:
|
|
217
|
-
stream_result(location, token, pa)
|
|
218
|
-
else:
|
|
219
|
-
poll_for_result(location, retry_later, token, is_silent, line)
|
|
220
|
-
|
|
221
216
|
except Exception as e:
|
|
222
217
|
line(f"<error>Failed to handle 202 response: {e}</error>")
|
|
218
|
+
|
|
219
|
+
if pa.stream:
|
|
220
|
+
stream_result(location, job_id, token, pa, is_silent, line)
|
|
221
|
+
else:
|
|
222
|
+
poll_for_result(location, job_id, retry_later, token, is_silent, line)
|
|
223
|
+
|
|
223
224
|
else:
|
|
224
225
|
handle_response(response, line)
|
|
225
226
|
|
|
@@ -248,7 +249,9 @@ def handle_response(resp, line):
|
|
|
248
249
|
line(f"<warning>Headers: {str(resp.headers)}</warning>")
|
|
249
250
|
return "unknown"
|
|
250
251
|
|
|
251
|
-
def poll_for_result(location, retry_later, token, is_silent, line):
|
|
252
|
+
def poll_for_result(location, job_id, retry_later, token, is_silent, line):
|
|
253
|
+
if not is_silent:
|
|
254
|
+
line(f"<debug>Job '{job_id}' accepted, but no result yet. Polling in {retry_later} seconds.</debug>")
|
|
252
255
|
while True:
|
|
253
256
|
time.sleep(retry_later)
|
|
254
257
|
poll_headers = {
|
|
@@ -273,35 +276,97 @@ def poll_for_result(location, retry_later, token, is_silent, line):
|
|
|
273
276
|
else:
|
|
274
277
|
break
|
|
275
278
|
|
|
276
|
-
|
|
279
|
+
CONNECT_TIMEOUT = 5
|
|
280
|
+
READ_TIMEOUT = 120 # ensure server sends heartbeat within this window
|
|
281
|
+
|
|
282
|
+
def stream_result(location, job_id, token, pa, is_silent, line):
|
|
277
283
|
"""
|
|
278
284
|
Stream the result content from the given location using the provided token.
|
|
279
285
|
"""
|
|
286
|
+
if not is_silent:
|
|
287
|
+
line(f"<debug>Job '{job_id}' accepted, Waiting for events now.</debug>")
|
|
280
288
|
headers = {
|
|
281
289
|
"Authorization": f"Bearer {token}",
|
|
282
|
-
"Accept": "text/event-stream"
|
|
290
|
+
"Accept": "text/event-stream",
|
|
291
|
+
"Cache-Control": "no-cache",
|
|
292
|
+
"Connection": "keep-alive",
|
|
283
293
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
294
|
+
url = location + "/events"
|
|
295
|
+
session = requests.Session()
|
|
296
|
+
last_event_id = None
|
|
297
|
+
backoff = 1.0
|
|
298
|
+
while True:
|
|
299
|
+
try:
|
|
300
|
+
if last_event_id:
|
|
301
|
+
headers["Last-Event-ID"] = last_event_id
|
|
302
|
+
with session.get(url, stream=True, headers=headers, timeout=(CONNECT_TIMEOUT, READ_TIMEOUT)) as r:
|
|
303
|
+
r.raise_for_status()
|
|
304
|
+
# Reset backoff on successful connect
|
|
305
|
+
backoff = 1.0
|
|
306
|
+
for row in r.iter_lines(decode_unicode=True, chunk_size=1):
|
|
307
|
+
if row is None:
|
|
308
|
+
continue
|
|
309
|
+
if row.startswith(":"):
|
|
310
|
+
# comment/heartbeat
|
|
311
|
+
continue
|
|
312
|
+
if row.startswith("id:"):
|
|
313
|
+
last_event_id = row[3:].strip()
|
|
314
|
+
continue
|
|
315
|
+
if print_sse_row(row, pa, line): # raw SSE lines (e.g., "data: {...}", "event: message")
|
|
316
|
+
return # done
|
|
317
|
+
|
|
318
|
+
except (requests.exceptions.ChunkedEncodingError,
|
|
319
|
+
requests.exceptions.ConnectionError,
|
|
320
|
+
requests.exceptions.ReadTimeout) as e:
|
|
321
|
+
if not is_silent:
|
|
322
|
+
line(f"<debug>stream error: {e}; reconnecting in {backoff:.1f}s</debug>")
|
|
323
|
+
time.sleep(backoff)
|
|
324
|
+
backoff = min(backoff * 2, 30.0) # cap backoff
|
|
325
|
+
continue
|
|
326
|
+
except requests.HTTPError as e:
|
|
327
|
+
# Non-200 or similar; backoff and retry
|
|
328
|
+
if not is_silent:
|
|
329
|
+
line(f"<debug>http error: {e}; reconnecting in {backoff:.1f}s</debug>")
|
|
330
|
+
time.sleep(backoff)
|
|
331
|
+
backoff = min(backoff * 2, 60.0)
|
|
332
|
+
except Exception:
|
|
333
|
+
line(f"<error>Failed to fetch events: {type(e)} - {e}</error>")
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
# except requests.exceptions.ChunkedEncodingError as ce:
|
|
337
|
+
# line(f"<error>Chunked encoding error: {ce}</error>")
|
|
338
|
+
# except Exception as e:
|
|
339
|
+
# line(f"<error>Failed to fetch events: {type(e)} - {e}</error>")
|
|
340
|
+
|
|
341
|
+
def print_sse_row(row, pa, line) -> bool:
|
|
295
342
|
if pa.raw_events:
|
|
296
343
|
print(row)
|
|
344
|
+
return False # continue streaming
|
|
297
345
|
elif row.startswith("data: "):
|
|
298
346
|
# JSON data
|
|
299
347
|
print("----")
|
|
300
348
|
try:
|
|
301
349
|
data = json.loads(row[6:])
|
|
302
350
|
print(json.dumps(data, indent=2, sort_keys=True))
|
|
351
|
+
return check_if_done(data)
|
|
303
352
|
except json.JSONDecodeError as e:
|
|
304
|
-
|
|
353
|
+
line(f"<error>Failed to decode JSON: {e}</error>")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def check_if_done(data) -> bool:
|
|
357
|
+
# {
|
|
358
|
+
# "Data": {
|
|
359
|
+
# "job-urn": "urn:ivcap:job:3e466031-ec8b-44eb-aec6-dc2f8212bec3",
|
|
360
|
+
# "status": "succeeded"
|
|
361
|
+
# },
|
|
362
|
+
# "Type": "ivcap.job.status"
|
|
363
|
+
# }
|
|
364
|
+
if "Type" in data and data["Type"] == "ivcap.job.status":
|
|
365
|
+
if "Data" in data and "status" in data["Data"]:
|
|
366
|
+
status = data["Data"]["status"]
|
|
367
|
+
if status in ["succeeded", "failed", "error"]:
|
|
368
|
+
return True
|
|
369
|
+
return False # continue streaming
|
|
305
370
|
|
|
306
371
|
def exec_parser():
|
|
307
372
|
p = argparse.ArgumentParser(prog="poetry ivcap job-exec request_file --")
|
|
@@ -310,6 +375,7 @@ def exec_parser():
|
|
|
310
375
|
help="include result content in the response")
|
|
311
376
|
p.add_argument("--stream", action="store_true", help="stream the result content")
|
|
312
377
|
p.add_argument("--raw-events", action="store_true", help="print raw SSE events")
|
|
378
|
+
p.add_argument("--auth-token", help="alternative auth token to use")
|
|
313
379
|
return p
|
|
314
380
|
|
|
315
381
|
def nonneg_int(s: str) -> int:
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
poetry_plugin_ivcap/constants.py,sha256=zsUnw-cS3uTqRlkAsWPYBeMpn5Wpz7r4ByjYaFZQFUo,1318
|
|
2
2
|
poetry_plugin_ivcap/docker.py,sha256=Pny5uF7juKy8Jld7D-5euAtgzguDZ8E2SRn-romk-Sg,7409
|
|
3
|
-
poetry_plugin_ivcap/ivcap.py,sha256=
|
|
3
|
+
poetry_plugin_ivcap/ivcap.py,sha256=K-wZES-7EIViTnUZ4fklVqIr_uV-6KuQXM2ziNAwFHw,15490
|
|
4
4
|
poetry_plugin_ivcap/plugin.py,sha256=NWYdqjvlw3cqg4DVcEcZLGAVuL1aa9o0GmQGNZ6wfbI,5506
|
|
5
5
|
poetry_plugin_ivcap/util.py,sha256=bcjjbKoV_pAgeuC7Ws9XbJa3phFpNVyrRAlFJ1VubRg,3429
|
|
6
|
-
poetry_plugin_ivcap-0.5.
|
|
7
|
-
poetry_plugin_ivcap-0.5.
|
|
8
|
-
poetry_plugin_ivcap-0.5.
|
|
9
|
-
poetry_plugin_ivcap-0.5.
|
|
10
|
-
poetry_plugin_ivcap-0.5.
|
|
11
|
-
poetry_plugin_ivcap-0.5.
|
|
6
|
+
poetry_plugin_ivcap-0.5.1.dist-info/AUTHORS.md,sha256=s9xR4_HAHQgbNlj505LViebt5AtACQmhPf92aJvNYgg,88
|
|
7
|
+
poetry_plugin_ivcap-0.5.1.dist-info/LICENSE,sha256=dsQrDPPwW7iJs9pxahgJKDW8RNPf5FyXG70MFUlxcuk,1587
|
|
8
|
+
poetry_plugin_ivcap-0.5.1.dist-info/METADATA,sha256=_USNLVLTjHMkREPWR3CXnsuYSeWvRZJFT7JAJhtnePg,3032
|
|
9
|
+
poetry_plugin_ivcap-0.5.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
10
|
+
poetry_plugin_ivcap-0.5.1.dist-info/entry_points.txt,sha256=3xagEFBkGgrVe8WyjmhlHLr4JDEWPN_W4DwxnIBWbNY,74
|
|
11
|
+
poetry_plugin_ivcap-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{poetry_plugin_ivcap-0.5.0.dist-info → poetry_plugin_ivcap-0.5.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|