datatoolpack 0.5.2__tar.gz → 0.6.0__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.
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/PKG-INFO +1 -1
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/datatoolpack/__init__.py +1 -1
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/datatoolpack/client.py +126 -18
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/datatoolpack.egg-info/PKG-INFO +1 -1
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/setup.py +1 -1
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/README.md +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/datatoolpack.egg-info/SOURCES.txt +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/datatoolpack.egg-info/dependency_links.txt +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/datatoolpack.egg-info/requires.txt +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/datatoolpack.egg-info/top_level.txt +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/pyproject.toml +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.6.0}/setup.cfg +0 -0
|
@@ -24,6 +24,8 @@ import time
|
|
|
24
24
|
import zipfile
|
|
25
25
|
from typing import Dict, List, Optional, Union
|
|
26
26
|
|
|
27
|
+
import requests
|
|
28
|
+
|
|
27
29
|
|
|
28
30
|
try:
|
|
29
31
|
import requests
|
|
@@ -344,16 +346,34 @@ class AutoDataClient:
|
|
|
344
346
|
return False
|
|
345
347
|
|
|
346
348
|
def wait_for_completion(
|
|
347
|
-
self,
|
|
349
|
+
self,
|
|
350
|
+
session_id: str,
|
|
351
|
+
poll_interval: int = 2,
|
|
352
|
+
timeout: Optional[float] = None,
|
|
353
|
+
stall_timeout: float = 600.0,
|
|
348
354
|
) -> Dict:
|
|
349
355
|
"""Block until a session reaches *completed*, *error*, or *cancelled*.
|
|
350
356
|
|
|
351
357
|
Prints progress updates to stdout.
|
|
352
358
|
|
|
359
|
+
Args:
|
|
360
|
+
session_id: Session to wait for.
|
|
361
|
+
poll_interval: Seconds between status polls (default 2).
|
|
362
|
+
timeout: Hard ceiling in seconds for the whole wait. ``None``
|
|
363
|
+
disables. Raises ``AutoDataError`` if exceeded.
|
|
364
|
+
stall_timeout: If progress (percent + message) doesn't change for
|
|
365
|
+
this many seconds AND status is still 'unknown' /
|
|
366
|
+
'processing', treat as a stalled worker and raise.
|
|
367
|
+
Default 10 minutes.
|
|
368
|
+
|
|
353
369
|
Raises:
|
|
354
|
-
AutoDataError: On processing failure or
|
|
370
|
+
AutoDataError: On processing failure, cancellation, timeout, or
|
|
371
|
+
detected stall.
|
|
355
372
|
"""
|
|
356
373
|
print(f"Waiting for session {session_id}…")
|
|
374
|
+
start = time.monotonic()
|
|
375
|
+
last_change = start
|
|
376
|
+
last_signature: tuple = ()
|
|
357
377
|
while True:
|
|
358
378
|
data = self.get_status(session_id)
|
|
359
379
|
status = data.get("status", "unknown")
|
|
@@ -363,11 +383,30 @@ class AutoDataClient:
|
|
|
363
383
|
if status == "completed":
|
|
364
384
|
print(f"\r✓ Completed ({pct}%): {msg} ")
|
|
365
385
|
return self.get_result(session_id)
|
|
366
|
-
|
|
386
|
+
if status == "error":
|
|
367
387
|
raise AutoDataError(f"Processing failed: {msg}")
|
|
368
|
-
|
|
388
|
+
if status == "cancelled":
|
|
369
389
|
raise AutoDataError("Processing was cancelled")
|
|
370
390
|
|
|
391
|
+
# Detect stalled workers: status stays 'unknown'/'processing' with
|
|
392
|
+
# no progress change. Prevents infinite loops when cpu_worker died.
|
|
393
|
+
sig = (status, pct, msg)
|
|
394
|
+
now = time.monotonic()
|
|
395
|
+
if sig != last_signature:
|
|
396
|
+
last_signature = sig
|
|
397
|
+
last_change = now
|
|
398
|
+
elif now - last_change > stall_timeout:
|
|
399
|
+
raise AutoDataError(
|
|
400
|
+
f"Processing stalled: no progress for {stall_timeout:.0f}s "
|
|
401
|
+
f"(last status={status!r}, message={msg!r}). "
|
|
402
|
+
"The worker may have crashed — please retry."
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
if timeout is not None and now - start > timeout:
|
|
406
|
+
raise AutoDataError(
|
|
407
|
+
f"wait_for_completion timed out after {timeout:.0f}s"
|
|
408
|
+
)
|
|
409
|
+
|
|
371
410
|
print(f"\r {pct:3d}% — {msg[:60]:<60}", end="", flush=True)
|
|
372
411
|
time.sleep(poll_interval)
|
|
373
412
|
|
|
@@ -411,16 +450,32 @@ class AutoDataClient:
|
|
|
411
450
|
"POST",
|
|
412
451
|
self._url(f"/download-archive/{session_id}"),
|
|
413
452
|
json=body,
|
|
414
|
-
timeout=max(self.timeout,
|
|
415
|
-
stream=True,
|
|
453
|
+
timeout=max(self.timeout, 900),
|
|
416
454
|
)
|
|
417
455
|
self._raise_for_error(r)
|
|
456
|
+
content = r.content or b""
|
|
457
|
+
if not content:
|
|
458
|
+
raise AutoDataError(
|
|
459
|
+
f"Server returned an empty archive for session {session_id} "
|
|
460
|
+
"(the outputs may have expired or failed to generate — please retry the run)."
|
|
461
|
+
)
|
|
418
462
|
try:
|
|
419
|
-
z = zipfile.ZipFile(io.BytesIO(
|
|
463
|
+
z = zipfile.ZipFile(io.BytesIO(content))
|
|
464
|
+
members = z.namelist()
|
|
465
|
+
if not members:
|
|
466
|
+
raise AutoDataError(
|
|
467
|
+
f"Archive for session {session_id} is empty "
|
|
468
|
+
"(no output files found on the server)."
|
|
469
|
+
)
|
|
420
470
|
z.extractall(download_path)
|
|
421
|
-
print(f"Extracted
|
|
471
|
+
print(f"Extracted {len(members)} file(s) to {download_path}")
|
|
422
472
|
except zipfile.BadZipFile:
|
|
423
|
-
|
|
473
|
+
# Surface the body head to aid debugging — often an HTML error page.
|
|
474
|
+
preview = content[:200].decode("utf-8", errors="replace")
|
|
475
|
+
raise AutoDataError(
|
|
476
|
+
f"Server returned an invalid ZIP archive "
|
|
477
|
+
f"(Content-Type={r.headers.get('Content-Type')!r}, head={preview!r})"
|
|
478
|
+
)
|
|
424
479
|
else:
|
|
425
480
|
result = self.get_result(session_id)
|
|
426
481
|
for f in result.get("files", []):
|
|
@@ -434,12 +489,26 @@ class AutoDataClient:
|
|
|
434
489
|
|
|
435
490
|
return os.path.abspath(download_path)
|
|
436
491
|
|
|
437
|
-
def download_file(
|
|
492
|
+
def download_file(
|
|
493
|
+
self,
|
|
494
|
+
url: str,
|
|
495
|
+
output_path: str,
|
|
496
|
+
timeout: Optional[float] = None,
|
|
497
|
+
retries: int = 2,
|
|
498
|
+
) -> None:
|
|
438
499
|
"""Download a single file by URL (absolute or server-relative).
|
|
439
500
|
|
|
501
|
+
Validates Content-Length when the server provides it, and retries on
|
|
502
|
+
transient network errors. Partial downloads are detected and raised
|
|
503
|
+
rather than silently succeeding.
|
|
504
|
+
|
|
440
505
|
Args:
|
|
441
506
|
url: Full URL or path starting with ``/``.
|
|
442
507
|
output_path: Local file path to write to.
|
|
508
|
+
timeout: Per-attempt timeout in seconds. Default scales with
|
|
509
|
+
the client's configured timeout; pass an explicit
|
|
510
|
+
value for very large files on slow networks.
|
|
511
|
+
retries: Number of retry attempts on partial / network errors.
|
|
443
512
|
"""
|
|
444
513
|
if not url.startswith("http"):
|
|
445
514
|
url = f"{self.base_url}{url}"
|
|
@@ -447,14 +516,53 @@ class AutoDataClient:
|
|
|
447
516
|
# Ensure parent directory exists
|
|
448
517
|
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
|
449
518
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
519
|
+
effective_timeout = timeout if timeout is not None else max(self.timeout, 900)
|
|
520
|
+
last_err: Optional[Exception] = None
|
|
521
|
+
|
|
522
|
+
for attempt in range(retries + 1):
|
|
523
|
+
bytes_written = 0
|
|
524
|
+
expected: Optional[int] = None
|
|
525
|
+
try:
|
|
526
|
+
r = self._request(
|
|
527
|
+
"GET", url, stream=True, timeout=effective_timeout
|
|
528
|
+
)
|
|
529
|
+
self._raise_for_error(r)
|
|
530
|
+
# Reject 0-byte responses so the caller notices instead of
|
|
531
|
+
# silently producing an empty file.
|
|
532
|
+
cl = r.headers.get("Content-Length")
|
|
533
|
+
if cl is not None:
|
|
534
|
+
try:
|
|
535
|
+
expected = int(cl)
|
|
536
|
+
except (TypeError, ValueError):
|
|
537
|
+
expected = None
|
|
538
|
+
if expected == 0:
|
|
539
|
+
raise AutoDataError(
|
|
540
|
+
f"Server returned an empty file for {url} "
|
|
541
|
+
"(the output may have failed to generate — please retry the run)."
|
|
542
|
+
)
|
|
543
|
+
with open(output_path, "wb") as fh:
|
|
544
|
+
for chunk in r.iter_content(chunk_size=65536):
|
|
545
|
+
if not chunk:
|
|
546
|
+
continue
|
|
547
|
+
fh.write(chunk)
|
|
548
|
+
bytes_written += len(chunk)
|
|
549
|
+
if expected is not None and bytes_written != expected:
|
|
550
|
+
raise AutoDataError(
|
|
551
|
+
f"Partial download: received {bytes_written} of {expected} bytes"
|
|
552
|
+
)
|
|
553
|
+
print(f"Downloaded to {output_path} ({bytes_written} bytes)")
|
|
554
|
+
return
|
|
555
|
+
except (requests.ConnectionError, requests.Timeout, AutoDataError) as err:
|
|
556
|
+
last_err = err
|
|
557
|
+
if attempt < retries:
|
|
558
|
+
backoff = 2 ** attempt
|
|
559
|
+
print(f"\nDownload attempt {attempt + 1} failed ({err}); retrying in {backoff}s…")
|
|
560
|
+
time.sleep(backoff)
|
|
561
|
+
continue
|
|
562
|
+
raise
|
|
563
|
+
# Unreachable — loop either returns or re-raises
|
|
564
|
+
if last_err:
|
|
565
|
+
raise last_err
|
|
458
566
|
|
|
459
567
|
# ------------------------------------------------------------------
|
|
460
568
|
# Account / API key management
|
|
@@ -7,7 +7,7 @@ with open(os.path.join(here, "README.md"), encoding="utf-8") as f:
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name="datatoolpack",
|
|
10
|
-
version="0.
|
|
10
|
+
version="0.6.0",
|
|
11
11
|
description="Official Python SDK for the AutoData ML data preparation pipeline API",
|
|
12
12
|
long_description=long_description,
|
|
13
13
|
long_description_content_type="text/markdown",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|