datatoolpack 0.5.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datatoolpack
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Official Python SDK for the AutoData ML data preparation pipeline API
5
5
  Home-page: https://autodata.datatoolpack.com
6
6
  Author: AutoData Team
@@ -1,4 +1,4 @@
1
1
  from .client import AutoDataClient, AutoDataError
2
2
 
3
- __version__ = "0.5.1"
3
+ __version__ = "0.6.0"
4
4
  __all__ = ["AutoDataClient", "AutoDataError", "__version__"]
@@ -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, session_id: str, poll_interval: int = 2
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 cancellation.
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
- elif status == "error":
386
+ if status == "error":
367
387
  raise AutoDataError(f"Processing failed: {msg}")
368
- elif status == "cancelled":
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, 300),
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(r.content))
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 results to {download_path}")
471
+ print(f"Extracted {len(members)} file(s) to {download_path}")
422
472
  except zipfile.BadZipFile:
423
- raise AutoDataError("Server returned an invalid ZIP archive")
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,23 +489,80 @@ class AutoDataClient:
434
489
 
435
490
  return os.path.abspath(download_path)
436
491
 
437
- def download_file(self, url: str, output_path: str) -> None:
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}"
446
- r = self._request(
447
- "GET", url, stream=True, timeout=max(self.timeout, 300)
448
- )
449
- self._raise_for_error(r)
450
- with open(output_path, "wb") as fh:
451
- for chunk in r.iter_content(chunk_size=65536):
452
- fh.write(chunk)
453
- print(f"Downloaded to {output_path}")
515
+
516
+ # Ensure parent directory exists
517
+ os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
518
+
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
454
566
 
455
567
  # ------------------------------------------------------------------
456
568
  # Account / API key management
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datatoolpack
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Official Python SDK for the AutoData ML data preparation pipeline API
5
5
  Home-page: https://autodata.datatoolpack.com
6
6
  Author: AutoData Team
@@ -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.5.1",
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