boti-dask 1.0.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.
@@ -0,0 +1,174 @@
1
+ Metadata-Version: 2.4
2
+ Name: boti-dask
3
+ Version: 1.0.0
4
+ Summary: Dask runtime and resilience utilities for the Boti ecosystem
5
+ Author-email: Luis Valverde <lvalverdeb@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/lvalverdeb/boti-dask
8
+ Project-URL: Repository, https://github.com/lvalverdeb/boti-dask
9
+ Project-URL: Documentation, https://github.com/lvalverdeb/boti-dask#readme
10
+ Project-URL: Issues, https://github.com/lvalverdeb/boti-dask/issues
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: boti>=0.1.0
24
+ Requires-Dist: dask[dataframe,distributed]>=2026.3.0
25
+ Requires-Dist: pandas>=3.0.2
26
+ Provides-Extra: extra
27
+ Requires-Dist: numpy>=2.3.0; extra == "extra"
28
+ Requires-Dist: polars>=1.29.0; extra == "extra"
29
+ Requires-Dist: pyarrow>=23.0.1; extra == "extra"
30
+
31
+ # boti-dask
32
+
33
+ Dask runtime/session/resilience utilities for the Boti ecosystem.
34
+
35
+ ## API sections
36
+
37
+ - Session: `docs/api/session.md`
38
+ - Resilience: `docs/api/resilience.md`
39
+ - Diagnostics: `docs/api/diagnostics.md`
40
+
41
+ ## Phase-1 scope
42
+
43
+ This initial bootstrap provides:
44
+
45
+ - `DaskSession` and `dask_session(...)` helpers
46
+ - `DaskSessionSettings` and `dask_session_from_env_prefix(...)` helpers
47
+ - shared-session lifecycle support
48
+ - recommended Dask config profile helpers
49
+ - resilient execution wrappers:
50
+ - `safe_compute`, `safe_persist`, `safe_wait`, `safe_head`, `safe_gather`
51
+ - async counterparts
52
+ - diagnostics helpers:
53
+ - `inspect_graph`, `describe_frame`, `diagnostics_logger`
54
+ - Dask emptiness and unique-value helpers:
55
+ - `dask_is_probably_empty`, `dask_is_empty`, `UniqueValuesExtractor`
56
+
57
+ ## Quick start
58
+
59
+ ```python
60
+ from boti_dask import dask_session, inspect_graph, safe_compute
61
+ import dask
62
+
63
+ with dask_session(cluster_kwargs={"n_workers": 1, "threads_per_worker": 1, "processes": False, "dashboard_address": ":0"}) as client:
64
+ value = dask.delayed(lambda: 6 * 7)()
65
+ print(safe_compute(value, dask_client=client))
66
+ print(inspect_graph(value))
67
+ ```
68
+
69
+ If `dashboard_address` is omitted for local managed sessions, `boti-dask` defaults it to `":0"`.
70
+
71
+ Load session defaults from prefixed environment variables:
72
+
73
+ ```python
74
+ from boti_dask import DaskSessionSettings, dask_session_from_env_prefix
75
+
76
+ settings = DaskSessionSettings.from_env_prefix("BOTI_DASK_", env_file=".env")
77
+ session = dask_session_from_env_prefix("BOTI_DASK_", env_file=".env", verify_connectivity=True)
78
+ ```
79
+
80
+ ## Distributed computing scenarios
81
+
82
+ ### Multi-worker cluster with grouped aggregation
83
+
84
+ Launch a 4-worker cluster, distribute a DataFrame across 8 partitions, and
85
+ run a grouped sum that executes in parallel across all workers:
86
+
87
+ ```python
88
+ from boti_dask import dask_session, describe_client, safe_persist, safe_compute
89
+ import dask.dataframe as dd, pandas as pd
90
+
91
+ with dask_session(
92
+ cluster_kwargs={"n_workers": 4, "threads_per_worker": 2, "processes": False, "dashboard_address": ":0"},
93
+ ) as client:
94
+ print(describe_client(client)) # "workers": 4, "threads": 8
95
+
96
+ ddf = dd.from_pandas(pd.DataFrame({"v": range(10_000), "g": ["X","Y","Z"] * 3334}), npartitions=8)
97
+ persisted = safe_persist(ddf, dask_client=client)
98
+ grouped = safe_compute(persisted.groupby("g")["v"].sum(), dask_client=client)
99
+ print(grouped) # {"X": 16665000, "Y": 16665000, "Z": 16665000}
100
+ ```
101
+
102
+ ### Shared session with nested contexts
103
+
104
+ Multiple ``with`` blocks share the same Dask client via reference counting.
105
+ The client stays alive until the last holder exits:
106
+
107
+ ```python
108
+ from boti_dask import dask_session
109
+
110
+ kwargs = {"n_workers": 2, "threads_per_worker": 1, "processes": False, "dashboard_address": ":0"}
111
+ with dask_session(cluster_kwargs=kwargs, shared=True, shared_key="my-cluster") as outer:
112
+ with dask_session(shared=True, shared_key="my-cluster") as inner:
113
+ print(outer is inner) # True
114
+ # outer still alive here
115
+ ```
116
+
117
+ ### Async distributed pipeline
118
+
119
+ Persist, wait, head, compute, and gather — all through async-safe wrappers:
120
+
121
+ ```python
122
+ import asyncio
123
+ from boti_dask import dask_session, async_safe_persist, async_safe_head, async_safe_gather
124
+ import dask.dataframe as dd, pandas as pd, dask
125
+
126
+ async def pipeline():
127
+ ddf = dd.from_pandas(pd.DataFrame({"id": range(100)}), npartitions=4)
128
+ with dask_session(cluster_kwargs={"n_workers": 2, "processes": False, "dashboard_address": ":0"}) as client:
129
+ p = await async_safe_persist(ddf, dask_client=client)
130
+ preview = await async_safe_head(p, n=3, dask_client=client)
131
+ gathered = await async_safe_gather([dask.delayed(lambda: 42)()], dask_client=client)
132
+ return preview["id"].tolist(), gathered
133
+
134
+ asyncio.run(pipeline())
135
+ ```
136
+
137
+ ### Orphaned graph detection
138
+
139
+ When a Dask collection outlives the session that created it, operations
140
+ fail with a clear error instead of a cryptic Dask traceback:
141
+
142
+ ```python
143
+ with dask_session(cluster_kwargs={...}) as client:
144
+ persisted = safe_persist(ddf, dask_client=client)
145
+ # session closed — persisted is now orphaned
146
+ safe_head(persisted) # RuntimeError: orphaned from its original client/worker state
147
+ ```
148
+
149
+ Pass an external Dask `Client` to keep the client alive across session boundaries:
150
+
151
+ ```python
152
+ from dask.distributed import LocalCluster, Client
153
+ cluster = LocalCluster(...)
154
+ external = Client(cluster)
155
+ with dask_session(client=external) as session_client:
156
+ result = safe_compute(dask.delayed(lambda: 42)(), dask_client=session_client)
157
+ # external.close() # manual lifecycle
158
+ ```
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ uv sync --dev
164
+ uv run pytest -q
165
+ ```
166
+
167
+ ## Examples
168
+
169
+ ```bash
170
+ uv run python examples/data_facade_dask_resilience.py
171
+ uv run python examples/smoke_all_examples.py
172
+ ```
173
+
174
+ See `examples/README.md` for details.
@@ -0,0 +1,144 @@
1
+ # boti-dask
2
+
3
+ Dask runtime/session/resilience utilities for the Boti ecosystem.
4
+
5
+ ## API sections
6
+
7
+ - Session: `docs/api/session.md`
8
+ - Resilience: `docs/api/resilience.md`
9
+ - Diagnostics: `docs/api/diagnostics.md`
10
+
11
+ ## Phase-1 scope
12
+
13
+ This initial bootstrap provides:
14
+
15
+ - `DaskSession` and `dask_session(...)` helpers
16
+ - `DaskSessionSettings` and `dask_session_from_env_prefix(...)` helpers
17
+ - shared-session lifecycle support
18
+ - recommended Dask config profile helpers
19
+ - resilient execution wrappers:
20
+ - `safe_compute`, `safe_persist`, `safe_wait`, `safe_head`, `safe_gather`
21
+ - async counterparts
22
+ - diagnostics helpers:
23
+ - `inspect_graph`, `describe_frame`, `diagnostics_logger`
24
+ - Dask emptiness and unique-value helpers:
25
+ - `dask_is_probably_empty`, `dask_is_empty`, `UniqueValuesExtractor`
26
+
27
+ ## Quick start
28
+
29
+ ```python
30
+ from boti_dask import dask_session, inspect_graph, safe_compute
31
+ import dask
32
+
33
+ with dask_session(cluster_kwargs={"n_workers": 1, "threads_per_worker": 1, "processes": False, "dashboard_address": ":0"}) as client:
34
+ value = dask.delayed(lambda: 6 * 7)()
35
+ print(safe_compute(value, dask_client=client))
36
+ print(inspect_graph(value))
37
+ ```
38
+
39
+ If `dashboard_address` is omitted for local managed sessions, `boti-dask` defaults it to `":0"`.
40
+
41
+ Load session defaults from prefixed environment variables:
42
+
43
+ ```python
44
+ from boti_dask import DaskSessionSettings, dask_session_from_env_prefix
45
+
46
+ settings = DaskSessionSettings.from_env_prefix("BOTI_DASK_", env_file=".env")
47
+ session = dask_session_from_env_prefix("BOTI_DASK_", env_file=".env", verify_connectivity=True)
48
+ ```
49
+
50
+ ## Distributed computing scenarios
51
+
52
+ ### Multi-worker cluster with grouped aggregation
53
+
54
+ Launch a 4-worker cluster, distribute a DataFrame across 8 partitions, and
55
+ run a grouped sum that executes in parallel across all workers:
56
+
57
+ ```python
58
+ from boti_dask import dask_session, describe_client, safe_persist, safe_compute
59
+ import dask.dataframe as dd, pandas as pd
60
+
61
+ with dask_session(
62
+ cluster_kwargs={"n_workers": 4, "threads_per_worker": 2, "processes": False, "dashboard_address": ":0"},
63
+ ) as client:
64
+ print(describe_client(client)) # "workers": 4, "threads": 8
65
+
66
+ ddf = dd.from_pandas(pd.DataFrame({"v": range(10_000), "g": ["X","Y","Z"] * 3334}), npartitions=8)
67
+ persisted = safe_persist(ddf, dask_client=client)
68
+ grouped = safe_compute(persisted.groupby("g")["v"].sum(), dask_client=client)
69
+ print(grouped) # {"X": 16665000, "Y": 16665000, "Z": 16665000}
70
+ ```
71
+
72
+ ### Shared session with nested contexts
73
+
74
+ Multiple ``with`` blocks share the same Dask client via reference counting.
75
+ The client stays alive until the last holder exits:
76
+
77
+ ```python
78
+ from boti_dask import dask_session
79
+
80
+ kwargs = {"n_workers": 2, "threads_per_worker": 1, "processes": False, "dashboard_address": ":0"}
81
+ with dask_session(cluster_kwargs=kwargs, shared=True, shared_key="my-cluster") as outer:
82
+ with dask_session(shared=True, shared_key="my-cluster") as inner:
83
+ print(outer is inner) # True
84
+ # outer still alive here
85
+ ```
86
+
87
+ ### Async distributed pipeline
88
+
89
+ Persist, wait, head, compute, and gather — all through async-safe wrappers:
90
+
91
+ ```python
92
+ import asyncio
93
+ from boti_dask import dask_session, async_safe_persist, async_safe_head, async_safe_gather
94
+ import dask.dataframe as dd, pandas as pd, dask
95
+
96
+ async def pipeline():
97
+ ddf = dd.from_pandas(pd.DataFrame({"id": range(100)}), npartitions=4)
98
+ with dask_session(cluster_kwargs={"n_workers": 2, "processes": False, "dashboard_address": ":0"}) as client:
99
+ p = await async_safe_persist(ddf, dask_client=client)
100
+ preview = await async_safe_head(p, n=3, dask_client=client)
101
+ gathered = await async_safe_gather([dask.delayed(lambda: 42)()], dask_client=client)
102
+ return preview["id"].tolist(), gathered
103
+
104
+ asyncio.run(pipeline())
105
+ ```
106
+
107
+ ### Orphaned graph detection
108
+
109
+ When a Dask collection outlives the session that created it, operations
110
+ fail with a clear error instead of a cryptic Dask traceback:
111
+
112
+ ```python
113
+ with dask_session(cluster_kwargs={...}) as client:
114
+ persisted = safe_persist(ddf, dask_client=client)
115
+ # session closed — persisted is now orphaned
116
+ safe_head(persisted) # RuntimeError: orphaned from its original client/worker state
117
+ ```
118
+
119
+ Pass an external Dask `Client` to keep the client alive across session boundaries:
120
+
121
+ ```python
122
+ from dask.distributed import LocalCluster, Client
123
+ cluster = LocalCluster(...)
124
+ external = Client(cluster)
125
+ with dask_session(client=external) as session_client:
126
+ result = safe_compute(dask.delayed(lambda: 42)(), dask_client=session_client)
127
+ # external.close() # manual lifecycle
128
+ ```
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ uv sync --dev
134
+ uv run pytest -q
135
+ ```
136
+
137
+ ## Examples
138
+
139
+ ```bash
140
+ uv run python examples/data_facade_dask_resilience.py
141
+ uv run python examples/smoke_all_examples.py
142
+ ```
143
+
144
+ See `examples/README.md` for details.
@@ -0,0 +1,75 @@
1
+ [project]
2
+ name = "boti-dask"
3
+ version = "1.0.0"
4
+ description = "Dask runtime and resilience utilities for the Boti ecosystem"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ authors = [
9
+ {name = "Luis Valverde", email = "lvalverdeb@gmail.com"}
10
+ ]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Operating System :: OS Independent",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+ dependencies = [
24
+ "boti>=0.1.0",
25
+ "dask[dataframe,distributed]>=2026.3.0",
26
+ "pandas>=3.0.2",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ extra = [
31
+ "numpy>=2.3.0",
32
+ "polars>=1.29.0",
33
+ "pyarrow>=23.0.1",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/lvalverdeb/boti-dask"
38
+ Repository = "https://github.com/lvalverdeb/boti-dask"
39
+ Documentation = "https://github.com/lvalverdeb/boti-dask#readme"
40
+ Issues = "https://github.com/lvalverdeb/boti-dask/issues"
41
+
42
+ [build-system]
43
+ requires = ["setuptools>=80", "wheel"]
44
+ build-backend = "setuptools.build_meta"
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "pytest>=9.0.3",
49
+ "pytest-asyncio>=1.3.0",
50
+ "ruff>=0.11.0",
51
+ ]
52
+
53
+ [tool.ruff]
54
+ line-length = 100
55
+ target-version = "py310"
56
+
57
+ [tool.ruff.lint]
58
+ select = ["E", "F", "W", "I", "UP"]
59
+ ignore = ["E501"]
60
+
61
+ [tool.pytest.ini_options]
62
+ addopts = "-q"
63
+
64
+ [tool.setuptools]
65
+ include-package-data = true
66
+
67
+ [tool.setuptools.package-data]
68
+ boti_dask = ["py.typed"]
69
+
70
+ [tool.setuptools.package-dir]
71
+ "" = "src"
72
+
73
+ [tool.setuptools.packages.find]
74
+ where = ["src"]
75
+ include = ["boti_dask*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+
@@ -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"}
@@ -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
+ ]
File without changes