datatoolpack 0.5.2__tar.gz → 0.7.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.7.0}/PKG-INFO +1 -1
- {datatoolpack-0.5.2 → datatoolpack-0.7.0}/datatoolpack/__init__.py +1 -1
- {datatoolpack-0.5.2 → datatoolpack-0.7.0}/datatoolpack/client.py +253 -15
- {datatoolpack-0.5.2 → datatoolpack-0.7.0}/datatoolpack.egg-info/PKG-INFO +1 -1
- {datatoolpack-0.5.2 → datatoolpack-0.7.0}/setup.py +1 -1
- {datatoolpack-0.5.2 → datatoolpack-0.7.0}/README.md +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.7.0}/datatoolpack.egg-info/SOURCES.txt +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.7.0}/datatoolpack.egg-info/dependency_links.txt +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.7.0}/datatoolpack.egg-info/requires.txt +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.7.0}/datatoolpack.egg-info/top_level.txt +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.7.0}/pyproject.toml +0 -0
- {datatoolpack-0.5.2 → datatoolpack-0.7.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
|
|
@@ -212,6 +214,7 @@ class AutoDataClient:
|
|
|
212
214
|
auto_download: bool = True,
|
|
213
215
|
output_preferences: Optional[List[str]] = None,
|
|
214
216
|
compressed: bool = True,
|
|
217
|
+
return_dataframes: bool = False,
|
|
215
218
|
) -> Dict:
|
|
216
219
|
"""Upload a data file and start the AutoData pipeline.
|
|
217
220
|
|
|
@@ -305,6 +308,12 @@ class AutoDataClient:
|
|
|
305
308
|
output_preferences=output_preferences,
|
|
306
309
|
compressed=compressed,
|
|
307
310
|
)
|
|
311
|
+
# New in 0.7.0: return the output files as DataFrames so callers can
|
|
312
|
+
# skip the "download to disk, then pd.read_csv()" two-step. Gated on
|
|
313
|
+
# an opt-in flag to keep the default light (no pandas import).
|
|
314
|
+
if return_dataframes:
|
|
315
|
+
final = dict(final) # don't mutate the cached result object
|
|
316
|
+
final["dataframes"] = self.get_dataframes(session_id, names=output_preferences)
|
|
308
317
|
return final
|
|
309
318
|
|
|
310
319
|
def get_status(self, session_id: str) -> Dict:
|
|
@@ -344,16 +353,34 @@ class AutoDataClient:
|
|
|
344
353
|
return False
|
|
345
354
|
|
|
346
355
|
def wait_for_completion(
|
|
347
|
-
self,
|
|
356
|
+
self,
|
|
357
|
+
session_id: str,
|
|
358
|
+
poll_interval: int = 2,
|
|
359
|
+
timeout: Optional[float] = None,
|
|
360
|
+
stall_timeout: float = 600.0,
|
|
348
361
|
) -> Dict:
|
|
349
362
|
"""Block until a session reaches *completed*, *error*, or *cancelled*.
|
|
350
363
|
|
|
351
364
|
Prints progress updates to stdout.
|
|
352
365
|
|
|
366
|
+
Args:
|
|
367
|
+
session_id: Session to wait for.
|
|
368
|
+
poll_interval: Seconds between status polls (default 2).
|
|
369
|
+
timeout: Hard ceiling in seconds for the whole wait. ``None``
|
|
370
|
+
disables. Raises ``AutoDataError`` if exceeded.
|
|
371
|
+
stall_timeout: If progress (percent + message) doesn't change for
|
|
372
|
+
this many seconds AND status is still 'unknown' /
|
|
373
|
+
'processing', treat as a stalled worker and raise.
|
|
374
|
+
Default 10 minutes.
|
|
375
|
+
|
|
353
376
|
Raises:
|
|
354
|
-
AutoDataError: On processing failure or
|
|
377
|
+
AutoDataError: On processing failure, cancellation, timeout, or
|
|
378
|
+
detected stall.
|
|
355
379
|
"""
|
|
356
380
|
print(f"Waiting for session {session_id}…")
|
|
381
|
+
start = time.monotonic()
|
|
382
|
+
last_change = start
|
|
383
|
+
last_signature: tuple = ()
|
|
357
384
|
while True:
|
|
358
385
|
data = self.get_status(session_id)
|
|
359
386
|
status = data.get("status", "unknown")
|
|
@@ -363,11 +390,30 @@ class AutoDataClient:
|
|
|
363
390
|
if status == "completed":
|
|
364
391
|
print(f"\r✓ Completed ({pct}%): {msg} ")
|
|
365
392
|
return self.get_result(session_id)
|
|
366
|
-
|
|
393
|
+
if status == "error":
|
|
367
394
|
raise AutoDataError(f"Processing failed: {msg}")
|
|
368
|
-
|
|
395
|
+
if status == "cancelled":
|
|
369
396
|
raise AutoDataError("Processing was cancelled")
|
|
370
397
|
|
|
398
|
+
# Detect stalled workers: status stays 'unknown'/'processing' with
|
|
399
|
+
# no progress change. Prevents infinite loops when cpu_worker died.
|
|
400
|
+
sig = (status, pct, msg)
|
|
401
|
+
now = time.monotonic()
|
|
402
|
+
if sig != last_signature:
|
|
403
|
+
last_signature = sig
|
|
404
|
+
last_change = now
|
|
405
|
+
elif now - last_change > stall_timeout:
|
|
406
|
+
raise AutoDataError(
|
|
407
|
+
f"Processing stalled: no progress for {stall_timeout:.0f}s "
|
|
408
|
+
f"(last status={status!r}, message={msg!r}). "
|
|
409
|
+
"The worker may have crashed — please retry."
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if timeout is not None and now - start > timeout:
|
|
413
|
+
raise AutoDataError(
|
|
414
|
+
f"wait_for_completion timed out after {timeout:.0f}s"
|
|
415
|
+
)
|
|
416
|
+
|
|
371
417
|
print(f"\r {pct:3d}% — {msg[:60]:<60}", end="", flush=True)
|
|
372
418
|
time.sleep(poll_interval)
|
|
373
419
|
|
|
@@ -411,16 +457,32 @@ class AutoDataClient:
|
|
|
411
457
|
"POST",
|
|
412
458
|
self._url(f"/download-archive/{session_id}"),
|
|
413
459
|
json=body,
|
|
414
|
-
timeout=max(self.timeout,
|
|
415
|
-
stream=True,
|
|
460
|
+
timeout=max(self.timeout, 900),
|
|
416
461
|
)
|
|
417
462
|
self._raise_for_error(r)
|
|
463
|
+
content = r.content or b""
|
|
464
|
+
if not content:
|
|
465
|
+
raise AutoDataError(
|
|
466
|
+
f"Server returned an empty archive for session {session_id} "
|
|
467
|
+
"(the outputs may have expired or failed to generate — please retry the run)."
|
|
468
|
+
)
|
|
418
469
|
try:
|
|
419
|
-
z = zipfile.ZipFile(io.BytesIO(
|
|
470
|
+
z = zipfile.ZipFile(io.BytesIO(content))
|
|
471
|
+
members = z.namelist()
|
|
472
|
+
if not members:
|
|
473
|
+
raise AutoDataError(
|
|
474
|
+
f"Archive for session {session_id} is empty "
|
|
475
|
+
"(no output files found on the server)."
|
|
476
|
+
)
|
|
420
477
|
z.extractall(download_path)
|
|
421
|
-
print(f"Extracted
|
|
478
|
+
print(f"Extracted {len(members)} file(s) to {download_path}")
|
|
422
479
|
except zipfile.BadZipFile:
|
|
423
|
-
|
|
480
|
+
# Surface the body head to aid debugging — often an HTML error page.
|
|
481
|
+
preview = content[:200].decode("utf-8", errors="replace")
|
|
482
|
+
raise AutoDataError(
|
|
483
|
+
f"Server returned an invalid ZIP archive "
|
|
484
|
+
f"(Content-Type={r.headers.get('Content-Type')!r}, head={preview!r})"
|
|
485
|
+
)
|
|
424
486
|
else:
|
|
425
487
|
result = self.get_result(session_id)
|
|
426
488
|
for f in result.get("files", []):
|
|
@@ -434,12 +496,26 @@ class AutoDataClient:
|
|
|
434
496
|
|
|
435
497
|
return os.path.abspath(download_path)
|
|
436
498
|
|
|
437
|
-
def download_file(
|
|
499
|
+
def download_file(
|
|
500
|
+
self,
|
|
501
|
+
url: str,
|
|
502
|
+
output_path: str,
|
|
503
|
+
timeout: Optional[float] = None,
|
|
504
|
+
retries: int = 2,
|
|
505
|
+
) -> None:
|
|
438
506
|
"""Download a single file by URL (absolute or server-relative).
|
|
439
507
|
|
|
508
|
+
Validates Content-Length when the server provides it, and retries on
|
|
509
|
+
transient network errors. Partial downloads are detected and raised
|
|
510
|
+
rather than silently succeeding.
|
|
511
|
+
|
|
440
512
|
Args:
|
|
441
513
|
url: Full URL or path starting with ``/``.
|
|
442
514
|
output_path: Local file path to write to.
|
|
515
|
+
timeout: Per-attempt timeout in seconds. Default scales with
|
|
516
|
+
the client's configured timeout; pass an explicit
|
|
517
|
+
value for very large files on slow networks.
|
|
518
|
+
retries: Number of retry attempts on partial / network errors.
|
|
443
519
|
"""
|
|
444
520
|
if not url.startswith("http"):
|
|
445
521
|
url = f"{self.base_url}{url}"
|
|
@@ -447,14 +523,172 @@ class AutoDataClient:
|
|
|
447
523
|
# Ensure parent directory exists
|
|
448
524
|
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
|
449
525
|
|
|
526
|
+
effective_timeout = timeout if timeout is not None else max(self.timeout, 900)
|
|
527
|
+
last_err: Optional[Exception] = None
|
|
528
|
+
|
|
529
|
+
for attempt in range(retries + 1):
|
|
530
|
+
bytes_written = 0
|
|
531
|
+
expected: Optional[int] = None
|
|
532
|
+
try:
|
|
533
|
+
r = self._request(
|
|
534
|
+
"GET", url, stream=True, timeout=effective_timeout
|
|
535
|
+
)
|
|
536
|
+
self._raise_for_error(r)
|
|
537
|
+
# Reject 0-byte responses so the caller notices instead of
|
|
538
|
+
# silently producing an empty file.
|
|
539
|
+
cl = r.headers.get("Content-Length")
|
|
540
|
+
if cl is not None:
|
|
541
|
+
try:
|
|
542
|
+
expected = int(cl)
|
|
543
|
+
except (TypeError, ValueError):
|
|
544
|
+
expected = None
|
|
545
|
+
if expected == 0:
|
|
546
|
+
raise AutoDataError(
|
|
547
|
+
f"Server returned an empty file for {url} "
|
|
548
|
+
"(the output may have failed to generate — please retry the run)."
|
|
549
|
+
)
|
|
550
|
+
with open(output_path, "wb") as fh:
|
|
551
|
+
for chunk in r.iter_content(chunk_size=65536):
|
|
552
|
+
if not chunk:
|
|
553
|
+
continue
|
|
554
|
+
fh.write(chunk)
|
|
555
|
+
bytes_written += len(chunk)
|
|
556
|
+
if expected is not None and bytes_written != expected:
|
|
557
|
+
raise AutoDataError(
|
|
558
|
+
f"Partial download: received {bytes_written} of {expected} bytes"
|
|
559
|
+
)
|
|
560
|
+
print(f"Downloaded to {output_path} ({bytes_written} bytes)")
|
|
561
|
+
return
|
|
562
|
+
except (requests.ConnectionError, requests.Timeout, AutoDataError) as err:
|
|
563
|
+
last_err = err
|
|
564
|
+
if attempt < retries:
|
|
565
|
+
backoff = 2 ** attempt
|
|
566
|
+
print(f"\nDownload attempt {attempt + 1} failed ({err}); retrying in {backoff}s…")
|
|
567
|
+
time.sleep(backoff)
|
|
568
|
+
continue
|
|
569
|
+
raise
|
|
570
|
+
# Unreachable — loop either returns or re-raises
|
|
571
|
+
if last_err:
|
|
572
|
+
raise last_err
|
|
573
|
+
|
|
574
|
+
# ------------------------------------------------------------------
|
|
575
|
+
# DataFrame-native helpers
|
|
576
|
+
# ------------------------------------------------------------------
|
|
577
|
+
# These let callers skip the "download to disk, then pd.read_csv()" two-step.
|
|
578
|
+
# pandas is imported lazily so the SDK stays light when a caller doesn't
|
|
579
|
+
# need it (the package install_requires only `requests`).
|
|
580
|
+
|
|
581
|
+
@staticmethod
|
|
582
|
+
def _require_pandas():
|
|
583
|
+
try:
|
|
584
|
+
import pandas as pd # noqa: F401
|
|
585
|
+
return pd
|
|
586
|
+
except ImportError as e:
|
|
587
|
+
raise AutoDataError(
|
|
588
|
+
"pandas is required for DataFrame helpers — install it with `pip install pandas` "
|
|
589
|
+
"or use download_file()/download_results() to get raw files instead."
|
|
590
|
+
) from e
|
|
591
|
+
|
|
592
|
+
def get_dataframe(
|
|
593
|
+
self,
|
|
594
|
+
session_id: str,
|
|
595
|
+
output_name: str,
|
|
596
|
+
*,
|
|
597
|
+
timeout: Optional[float] = None,
|
|
598
|
+
**read_csv_kwargs,
|
|
599
|
+
):
|
|
600
|
+
"""Fetch one output file and return it as a pandas DataFrame.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
session_id: Completed processing session.
|
|
604
|
+
output_name: File base name (e.g. ``"dsg_output"``, ``"mdh_output"``)
|
|
605
|
+
or the full filename (``"dsg_output.csv"``). ``.csv``
|
|
606
|
+
is appended if missing.
|
|
607
|
+
timeout: Per-request timeout; defaults to the SDK's normal
|
|
608
|
+
download timeout (15 min).
|
|
609
|
+
read_csv_kwargs: Forwarded to ``pandas.read_csv`` (e.g. ``dtype``,
|
|
610
|
+
``parse_dates``, ``usecols``).
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
A pandas.DataFrame.
|
|
614
|
+
|
|
615
|
+
Raises:
|
|
616
|
+
AutoDataError: if pandas isn't installed, the file is missing or
|
|
617
|
+
empty (server failed to generate it), or the
|
|
618
|
+
download was truncated.
|
|
619
|
+
"""
|
|
620
|
+
pd = self._require_pandas()
|
|
621
|
+
fname = output_name if output_name.lower().endswith(
|
|
622
|
+
(".csv", ".json", ".parquet", ".feather", ".orc")
|
|
623
|
+
) else f"{output_name}.csv"
|
|
624
|
+
url = self._url(f"/download/{session_id}/{fname}")
|
|
625
|
+
if not url.startswith("http"):
|
|
626
|
+
url = f"{self.base_url}{url}"
|
|
450
627
|
r = self._request(
|
|
451
|
-
"GET", url, stream=True,
|
|
628
|
+
"GET", url, stream=True,
|
|
629
|
+
timeout=timeout if timeout is not None else max(self.timeout, 900),
|
|
452
630
|
)
|
|
453
631
|
self._raise_for_error(r)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
632
|
+
cl = r.headers.get("Content-Length")
|
|
633
|
+
if cl == "0":
|
|
634
|
+
raise AutoDataError(
|
|
635
|
+
f"Server returned an empty file for {fname} — the output likely "
|
|
636
|
+
"failed to generate. Retry the run or check /result/<session_id>."
|
|
637
|
+
)
|
|
638
|
+
# Buffer into memory — pandas.read_csv needs a seekable stream and
|
|
639
|
+
# output files are bounded by the server-side row ceiling anyway.
|
|
640
|
+
body = r.content
|
|
641
|
+
if not body:
|
|
642
|
+
raise AutoDataError(f"Server returned 0 bytes for {fname}")
|
|
643
|
+
import io as _io
|
|
644
|
+
buf = _io.BytesIO(body)
|
|
645
|
+
low = fname.lower()
|
|
646
|
+
if low.endswith(".parquet"):
|
|
647
|
+
return pd.read_parquet(buf)
|
|
648
|
+
if low.endswith(".feather"):
|
|
649
|
+
return pd.read_feather(buf)
|
|
650
|
+
if low.endswith(".orc"):
|
|
651
|
+
return pd.read_orc(buf)
|
|
652
|
+
if low.endswith(".json"):
|
|
653
|
+
return pd.read_json(buf, **read_csv_kwargs)
|
|
654
|
+
return pd.read_csv(buf, **read_csv_kwargs)
|
|
655
|
+
|
|
656
|
+
def get_dataframes(
|
|
657
|
+
self,
|
|
658
|
+
session_id: str,
|
|
659
|
+
names: Optional[List[str]] = None,
|
|
660
|
+
*,
|
|
661
|
+
timeout: Optional[float] = None,
|
|
662
|
+
) -> Dict:
|
|
663
|
+
"""Fetch multiple output files and return a ``{name: DataFrame}`` dict.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
session_id: Completed processing session.
|
|
667
|
+
names: Output base names to fetch. ``None`` means every file
|
|
668
|
+
the server lists for the session (skipping PDF
|
|
669
|
+
reports and .json metadata files, which aren't
|
|
670
|
+
naturally a DataFrame).
|
|
671
|
+
timeout: Per-file timeout; see ``get_dataframe``.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
Dict of ``{file_stem: pandas.DataFrame}``.
|
|
675
|
+
"""
|
|
676
|
+
self._require_pandas()
|
|
677
|
+
if names is None:
|
|
678
|
+
result = self.get_result(session_id)
|
|
679
|
+
names = []
|
|
680
|
+
for f in result.get("files", []):
|
|
681
|
+
fn = f.get("name", "")
|
|
682
|
+
low = fn.lower()
|
|
683
|
+
if not low or low.endswith((".pdf", ".json", ".md")):
|
|
684
|
+
continue
|
|
685
|
+
names.append(fn)
|
|
686
|
+
out: Dict = {}
|
|
687
|
+
for n in names:
|
|
688
|
+
# Strip the extension so callers can access via the logical name.
|
|
689
|
+
key = n.rsplit(".", 1)[0] if "." in n else n
|
|
690
|
+
out[key] = self.get_dataframe(session_id, n, timeout=timeout)
|
|
691
|
+
return out
|
|
458
692
|
|
|
459
693
|
# ------------------------------------------------------------------
|
|
460
694
|
# Account / API key management
|
|
@@ -591,6 +825,7 @@ class AutoDataClient:
|
|
|
591
825
|
compressed: bool = True,
|
|
592
826
|
incremental: bool = False,
|
|
593
827
|
type_casts: Optional[Dict[str, str]] = None,
|
|
828
|
+
return_dataframes: bool = False,
|
|
594
829
|
) -> Dict:
|
|
595
830
|
"""Run the AutoData pipeline on data from a connector source.
|
|
596
831
|
|
|
@@ -691,6 +926,9 @@ class AutoDataClient:
|
|
|
691
926
|
output_preferences=output_preferences,
|
|
692
927
|
compressed=compressed,
|
|
693
928
|
)
|
|
929
|
+
if return_dataframes:
|
|
930
|
+
final = dict(final)
|
|
931
|
+
final["dataframes"] = self.get_dataframes(session_id, names=output_preferences)
|
|
694
932
|
return final
|
|
695
933
|
|
|
696
934
|
def write_output(
|
|
@@ -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.7.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
|