boti-dask 1.0.0__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.
- boti_dask/__init__.py +62 -0
- boti_dask/_internal.py +23 -0
- boti_dask/diagnostics.py +146 -0
- boti_dask/py.typed +0 -0
- boti_dask/resilience.py +557 -0
- boti_dask/session.py +671 -0
- boti_dask-1.0.0.dist-info/METADATA +174 -0
- boti_dask-1.0.0.dist-info/RECORD +10 -0
- boti_dask-1.0.0.dist-info/WHEEL +5 -0
- boti_dask-1.0.0.dist-info/top_level.txt +1 -0
boti_dask/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Dask runtime and resilience utilities for the Boti ecosystem."""
|
|
2
|
+
|
|
3
|
+
from boti_dask.diagnostics import (
|
|
4
|
+
UniqueValuesExtractor,
|
|
5
|
+
describe_frame,
|
|
6
|
+
diagnostics_logger,
|
|
7
|
+
inspect_graph,
|
|
8
|
+
)
|
|
9
|
+
from boti_dask.resilience import (
|
|
10
|
+
RECOVERABLE_DASK_ERRORS,
|
|
11
|
+
async_safe_compute,
|
|
12
|
+
async_safe_gather,
|
|
13
|
+
async_safe_head,
|
|
14
|
+
async_safe_persist,
|
|
15
|
+
async_safe_wait,
|
|
16
|
+
dask_is_empty,
|
|
17
|
+
dask_is_probably_empty,
|
|
18
|
+
safe_compute,
|
|
19
|
+
safe_gather,
|
|
20
|
+
safe_head,
|
|
21
|
+
safe_persist,
|
|
22
|
+
safe_wait,
|
|
23
|
+
)
|
|
24
|
+
from boti_dask.session import (
|
|
25
|
+
DaskSession,
|
|
26
|
+
DaskSessionSettings,
|
|
27
|
+
apply_recommended_dask_config,
|
|
28
|
+
current_client_summary,
|
|
29
|
+
dask_session,
|
|
30
|
+
dask_session_from_env_prefix,
|
|
31
|
+
describe_client,
|
|
32
|
+
recommended_dask_config,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"DaskSession",
|
|
37
|
+
"DaskSessionSettings",
|
|
38
|
+
"RECOVERABLE_DASK_ERRORS",
|
|
39
|
+
"UniqueValuesExtractor",
|
|
40
|
+
"apply_recommended_dask_config",
|
|
41
|
+
"async_safe_compute",
|
|
42
|
+
"async_safe_gather",
|
|
43
|
+
"async_safe_head",
|
|
44
|
+
"async_safe_persist",
|
|
45
|
+
"async_safe_wait",
|
|
46
|
+
"current_client_summary",
|
|
47
|
+
"dask_is_empty",
|
|
48
|
+
"dask_is_probably_empty",
|
|
49
|
+
"dask_session",
|
|
50
|
+
"dask_session_from_env_prefix",
|
|
51
|
+
"describe_frame",
|
|
52
|
+
"describe_client",
|
|
53
|
+
"diagnostics_logger",
|
|
54
|
+
"inspect_graph",
|
|
55
|
+
"recommended_dask_config",
|
|
56
|
+
"safe_compute",
|
|
57
|
+
"safe_gather",
|
|
58
|
+
"safe_head",
|
|
59
|
+
"safe_persist",
|
|
60
|
+
"safe_wait",
|
|
61
|
+
]
|
|
62
|
+
|
boti_dask/_internal.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import dask.dataframe as dd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _log(logger: Any | None, level: str, message: str) -> None:
|
|
9
|
+
if logger is None:
|
|
10
|
+
return
|
|
11
|
+
log_fn = getattr(logger, level, None)
|
|
12
|
+
if callable(log_fn):
|
|
13
|
+
log_fn(message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_dask_dataframe_like(obj: Any) -> bool:
|
|
17
|
+
return isinstance(obj, (dd.DataFrame, dd.Series)) or hasattr(obj, "_meta")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_running_client(client: Any | None) -> bool:
|
|
21
|
+
if client is None:
|
|
22
|
+
return False
|
|
23
|
+
return getattr(client, "status", "running") in {"running", "started"}
|
boti_dask/diagnostics.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import dask.dataframe as dd
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import pyarrow as pa
|
|
11
|
+
except ImportError:
|
|
12
|
+
pa = None
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import polars as pl
|
|
16
|
+
except ImportError:
|
|
17
|
+
pl = None
|
|
18
|
+
|
|
19
|
+
from boti.core.logger import Logger
|
|
20
|
+
|
|
21
|
+
from ._internal import _log
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _has_dask_graph(obj: Any) -> bool:
|
|
25
|
+
return hasattr(obj, "__dask_graph__")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def inspect_graph(obj: Any, *, logger: Any | None = None) -> dict[str, Any]:
|
|
29
|
+
"""Return compact Dask graph metrics for diagnostics and dry-run usage."""
|
|
30
|
+
if not _has_dask_graph(obj):
|
|
31
|
+
metrics = {"type": type(obj).__name__, "is_dask": False}
|
|
32
|
+
_log(logger, "info", f"Dask graph inspection metrics={metrics}")
|
|
33
|
+
return metrics
|
|
34
|
+
|
|
35
|
+
graph = obj.__dask_graph__()
|
|
36
|
+
graph_layers: int | None = None
|
|
37
|
+
if hasattr(getattr(obj, "dask", None), "layers"):
|
|
38
|
+
graph_layers = len(obj.dask.layers)
|
|
39
|
+
elif hasattr(graph, "layers"):
|
|
40
|
+
graph_layers = len(graph.layers)
|
|
41
|
+
metrics = {
|
|
42
|
+
"type": type(obj).__name__,
|
|
43
|
+
"is_dask": True,
|
|
44
|
+
"task_count": len(graph) if hasattr(graph, "__len__") else None,
|
|
45
|
+
"npartitions": getattr(obj, "npartitions", None),
|
|
46
|
+
"graph_layers": graph_layers,
|
|
47
|
+
}
|
|
48
|
+
_log(logger, "info", f"Dask graph inspection metrics={metrics}")
|
|
49
|
+
return metrics
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def describe_frame(frame: Any) -> dict[str, Any]:
|
|
53
|
+
"""Return compact frame metrics suitable for runtime diagnostics logs."""
|
|
54
|
+
if isinstance(frame, dd.DataFrame):
|
|
55
|
+
graph = frame.__dask_graph__()
|
|
56
|
+
return {
|
|
57
|
+
"engine": "dask",
|
|
58
|
+
"columns": len(frame.columns),
|
|
59
|
+
"npartitions": frame.npartitions,
|
|
60
|
+
"graph_tasks": len(graph) if hasattr(graph, "__len__") else None,
|
|
61
|
+
"graph_layers": len(frame.dask.layers) if hasattr(frame.dask, "layers") else None,
|
|
62
|
+
"known_divisions": frame.known_divisions,
|
|
63
|
+
}
|
|
64
|
+
if isinstance(frame, pd.DataFrame):
|
|
65
|
+
return {
|
|
66
|
+
"engine": "pandas",
|
|
67
|
+
"rows": len(frame.index),
|
|
68
|
+
"columns": len(frame.columns),
|
|
69
|
+
}
|
|
70
|
+
if pa is not None and isinstance(frame, pa.Table):
|
|
71
|
+
return {
|
|
72
|
+
"engine": "arrow",
|
|
73
|
+
"rows": frame.num_rows,
|
|
74
|
+
"columns": len(frame.column_names),
|
|
75
|
+
}
|
|
76
|
+
if pl is not None and isinstance(frame, pl.DataFrame):
|
|
77
|
+
return {
|
|
78
|
+
"engine": "polars",
|
|
79
|
+
"rows": frame.height,
|
|
80
|
+
"columns": frame.width,
|
|
81
|
+
}
|
|
82
|
+
return {"engine": type(frame).__name__}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def diagnostics_logger(logger: Any | None, *, name: str) -> Any:
|
|
86
|
+
"""Return provided logger or a :class:`boti.Logger` fallback scoped by *name*."""
|
|
87
|
+
if logger is not None:
|
|
88
|
+
return logger
|
|
89
|
+
return Logger.default_logger(logger_name=name)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class UniqueValuesExtractor:
|
|
93
|
+
"""Best-effort unique value extraction for Dask-backed columns."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, *, dask_client: Any | None = None, logger: Any | None = None) -> None:
|
|
96
|
+
self.dask_client = dask_client
|
|
97
|
+
self.logger = logger
|
|
98
|
+
|
|
99
|
+
def _extract_one(self, frame: dd.DataFrame, column: str, limit: int) -> tuple[str, list[Any]]:
|
|
100
|
+
from .resilience import safe_compute
|
|
101
|
+
|
|
102
|
+
if column not in frame.columns:
|
|
103
|
+
_log(self.logger, "warning", f"Column '{column}' not found for unique extraction.")
|
|
104
|
+
return column, []
|
|
105
|
+
|
|
106
|
+
expression = frame[column].dropna().drop_duplicates()
|
|
107
|
+
if self.dask_client is not None:
|
|
108
|
+
values = safe_compute(expression, dask_client=self.dask_client, logger=self.logger)
|
|
109
|
+
else:
|
|
110
|
+
values = expression.compute(scheduler="threads")
|
|
111
|
+
as_series = pd.Series(values)
|
|
112
|
+
unique_values = as_series.dropna().unique().tolist()
|
|
113
|
+
if len(unique_values) > limit:
|
|
114
|
+
_log(
|
|
115
|
+
self.logger,
|
|
116
|
+
"warning",
|
|
117
|
+
f"Unique value extraction for column '{column}' truncated at {limit} items.",
|
|
118
|
+
)
|
|
119
|
+
unique_values = unique_values[:limit]
|
|
120
|
+
return column, unique_values
|
|
121
|
+
|
|
122
|
+
async def extract_unique_values(
|
|
123
|
+
self,
|
|
124
|
+
frame: dd.DataFrame,
|
|
125
|
+
*columns: str,
|
|
126
|
+
limit: int = 100_000,
|
|
127
|
+
) -> dict[str, list[Any]]:
|
|
128
|
+
from .resilience import safe_persist
|
|
129
|
+
|
|
130
|
+
if self.dask_client is not None:
|
|
131
|
+
frame = safe_persist(frame, dask_client=self.dask_client, logger=self.logger)
|
|
132
|
+
else:
|
|
133
|
+
frame = frame.persist()
|
|
134
|
+
_log(self.logger, "info", f"Persisted {frame.npartitions}-partition frame for unique extraction.")
|
|
135
|
+
pairs = await asyncio.gather(
|
|
136
|
+
*(asyncio.to_thread(self._extract_one, frame, column, limit) for column in columns)
|
|
137
|
+
)
|
|
138
|
+
return dict(pairs)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
__all__ = [
|
|
142
|
+
"UniqueValuesExtractor",
|
|
143
|
+
"describe_frame",
|
|
144
|
+
"diagnostics_logger",
|
|
145
|
+
"inspect_graph",
|
|
146
|
+
]
|
boti_dask/py.typed
ADDED
|
File without changes
|