dataflux-core 0.1.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.
- dataflux/__init__.py +4 -0
- dataflux/__version__.py +1 -0
- dataflux/cache.py +122 -0
- dataflux/config.py +18 -0
- dataflux/display/__init__.py +0 -0
- dataflux/display/console.py +4 -0
- dataflux/display/info.py +150 -0
- dataflux/display/search.py +122 -0
- dataflux/display/theme.py +32 -0
- dataflux/display/utils.py +138 -0
- dataflux/exceptions.py +150 -0
- dataflux/export.py +62 -0
- dataflux/flux.py +55 -0
- dataflux/models/__init__.py +0 -0
- dataflux/models/dataset.py +15 -0
- dataflux/models/search_result.py +10 -0
- dataflux/providers/__init__.py +0 -0
- dataflux/providers/base.py +23 -0
- dataflux/providers/huggingface.py +91 -0
- dataflux/providers/kaggle.py +85 -0
- dataflux/providers/seaborn.py +66 -0
- dataflux/providers/sklearn.py +67 -0
- dataflux/providers/statsmodels.py +66 -0
- dataflux/providers/torchvision.py +80 -0
- dataflux/providers/uci.py +78 -0
- dataflux/providers/vega_datasets.py +64 -0
- dataflux/providers/worldbank.py +73 -0
- dataflux/registry.py +38 -0
- dataflux/resolver.py +30 -0
- dataflux/resources/kaggle_index.json +365901 -0
- dataflux/resources/seaborn_dataset.py +24 -0
- dataflux/resources/sklearn_dataset.py +17 -0
- dataflux/resources/statsmodels_dataset.py +30 -0
- dataflux/resources/torch_vision_dataset.py +89 -0
- dataflux/tests/conftest.py +33 -0
- dataflux/tests/display/test_info.py +159 -0
- dataflux/tests/display/test_search.py +83 -0
- dataflux/tests/models/test_dataset_info.py +46 -0
- dataflux/tests/models/test_search_result.py +28 -0
- dataflux/tests/providers/test_hugging.py +93 -0
- dataflux/tests/providers/test_kaggle.py +94 -0
- dataflux/tests/providers/test_seaborn.py +88 -0
- dataflux/tests/providers/test_sklearn.py +92 -0
- dataflux/tests/providers/test_statsmodels.py +89 -0
- dataflux/tests/providers/test_torchvision.py +90 -0
- dataflux/tests/providers/test_uci.py +92 -0
- dataflux/tests/providers/test_vega.py +90 -0
- dataflux/tests/providers/test_worldbank.py +111 -0
- dataflux/tests/resources/test_resources.py +80 -0
- dataflux/tests/test_exceptions.py +20 -0
- dataflux/tests/test_export.py +146 -0
- dataflux/tests/test_flux.py +40 -0
- dataflux/tests/test_models.py +30 -0
- dataflux/tests/test_registry.py +149 -0
- dataflux/tests/test_resolver.py +77 -0
- dataflux/tests/utils/test_cache.py +173 -0
- dataflux/tests/utils/test_download.py +135 -0
- dataflux/tests/utils/test_filesystem.py +128 -0
- dataflux/tests/utils/test_fingerprint.py +114 -0
- dataflux/tests/utils/test_search_utils.py +152 -0
- dataflux/utils/__init__.py +0 -0
- dataflux/utils/download.py +110 -0
- dataflux/utils/filesystem.py +89 -0
- dataflux/utils/fingerprint.py +155 -0
- dataflux/utils/search.py +66 -0
- dataflux_core-0.1.0.dist-info/METADATA +400 -0
- dataflux_core-0.1.0.dist-info/RECORD +69 -0
- dataflux_core-0.1.0.dist-info/WHEEL +4 -0
- dataflux_core-0.1.0.dist-info/licenses/LICENSE +21 -0
dataflux/__init__.py
ADDED
dataflux/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
dataflux/cache.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from dataflux.config import DATAFLUX_CACHE
|
|
9
|
+
from dataflux.utils.filesystem import ensure_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Cache:
|
|
13
|
+
"""
|
|
14
|
+
Generic JSON cache manager for DataFlux.
|
|
15
|
+
|
|
16
|
+
Examples
|
|
17
|
+
--------
|
|
18
|
+
>>> cache.write("search/iris", data)
|
|
19
|
+
>>> cache.read("search/iris")
|
|
20
|
+
>>> cache.exists("search/iris")
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, root: str | Path = DATAFLUX_CACHE):
|
|
24
|
+
self.root = ensure_dir(root)
|
|
25
|
+
|
|
26
|
+
# -------------------------------------------------------------------------
|
|
27
|
+
# Internal
|
|
28
|
+
# -------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def _path(self, key: str) -> Path:
|
|
31
|
+
"""
|
|
32
|
+
Convert a cache key into a JSON file path.
|
|
33
|
+
|
|
34
|
+
Example
|
|
35
|
+
-------
|
|
36
|
+
search/iris -> ~/.cache/dataflux/search/iris.json
|
|
37
|
+
"""
|
|
38
|
+
path = self.root / Path(key).with_suffix(".json")
|
|
39
|
+
ensure_dir(path.parent)
|
|
40
|
+
return path
|
|
41
|
+
|
|
42
|
+
# -------------------------------------------------------------------------
|
|
43
|
+
# Public API
|
|
44
|
+
# -------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def exists(self, key: str) -> bool:
|
|
47
|
+
"""Return True if a cache entry exists."""
|
|
48
|
+
return self._path(key).exists()
|
|
49
|
+
|
|
50
|
+
def read(self, key: str, default: Any = None) -> Any:
|
|
51
|
+
"""
|
|
52
|
+
Read cached JSON data.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
Cached object or `default` if the key doesn't exist.
|
|
57
|
+
"""
|
|
58
|
+
path = self._path(key)
|
|
59
|
+
|
|
60
|
+
if not path.exists():
|
|
61
|
+
return default
|
|
62
|
+
|
|
63
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
64
|
+
return json.load(f)
|
|
65
|
+
|
|
66
|
+
def write(self, key: str, value: Any) -> Path:
|
|
67
|
+
"""
|
|
68
|
+
Write JSON-serializable data to cache.
|
|
69
|
+
"""
|
|
70
|
+
path = self._path(key)
|
|
71
|
+
|
|
72
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
73
|
+
json.dump(
|
|
74
|
+
value,
|
|
75
|
+
f,
|
|
76
|
+
indent=2,
|
|
77
|
+
ensure_ascii=False,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return path
|
|
81
|
+
|
|
82
|
+
def delete(self, key: str) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Delete a cached object.
|
|
85
|
+
"""
|
|
86
|
+
path = self._path(key)
|
|
87
|
+
|
|
88
|
+
if not path.exists():
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
path.unlink()
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def clear(self) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Remove every cached object.
|
|
97
|
+
"""
|
|
98
|
+
shutil.rmtree(self.root, ignore_errors=True)
|
|
99
|
+
ensure_dir(self.root)
|
|
100
|
+
|
|
101
|
+
def list(self, pattern: str = "**/*.json") -> list[str]:
|
|
102
|
+
"""
|
|
103
|
+
List cached keys.
|
|
104
|
+
|
|
105
|
+
Examples
|
|
106
|
+
--------
|
|
107
|
+
>>> cache.list()
|
|
108
|
+
>>> cache.list("search/*.json")
|
|
109
|
+
"""
|
|
110
|
+
return sorted(
|
|
111
|
+
str(path.relative_to(self.root).with_suffix(""))
|
|
112
|
+
for path in self.root.glob(pattern)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def size(self) -> int:
|
|
116
|
+
"""
|
|
117
|
+
Return the total number of cached objects.
|
|
118
|
+
"""
|
|
119
|
+
return len(self.list())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
cache = Cache()
|
dataflux/config.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
DATAFLUX_CACHE = Path.home() / ".dataflux"
|
|
4
|
+
|
|
5
|
+
UCI_CACHE = DATAFLUX_CACHE / "uci"
|
|
6
|
+
KAGGLE_CACHE = DATAFLUX_CACHE / "kaggle"
|
|
7
|
+
HF_CACHE = DATAFLUX_CACHE / "huggingface"
|
|
8
|
+
TORCHVISION_CACHE = DATAFLUX_CACHE / "torchvision"
|
|
9
|
+
TFDS_CACHE = DATAFLUX_CACHE / "tensorflow"
|
|
10
|
+
|
|
11
|
+
for cache in (
|
|
12
|
+
UCI_CACHE,
|
|
13
|
+
KAGGLE_CACHE,
|
|
14
|
+
HF_CACHE,
|
|
15
|
+
TORCHVISION_CACHE,
|
|
16
|
+
TFDS_CACHE,
|
|
17
|
+
):
|
|
18
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
File without changes
|
dataflux/display/info.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from textwrap import fill
|
|
3
|
+
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
|
|
8
|
+
from dataflux.display.console import console
|
|
9
|
+
from dataflux.models.dataset import DatasetInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DESCRIPTION_LIMIT = 600
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _truncate(text: str | None) -> str | None:
|
|
16
|
+
"""Truncate long descriptions for prettier display."""
|
|
17
|
+
if not text:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
text = " ".join(text.split())
|
|
21
|
+
|
|
22
|
+
if len(text) <= DESCRIPTION_LIMIT:
|
|
23
|
+
return fill(text, width=80)
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
fill(text[:DESCRIPTION_LIMIT].rsplit(" ", 1)[0], width=80)
|
|
27
|
+
+ "\n\n[dim]... (description truncated)[/dim]"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _format(value):
|
|
32
|
+
"""Pretty-format common Python objects."""
|
|
33
|
+
|
|
34
|
+
if value is None:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
if isinstance(value, bool):
|
|
38
|
+
return "[success]Yes[/success]" if value else "[error]No[/error]"
|
|
39
|
+
|
|
40
|
+
if isinstance(value, Mapping):
|
|
41
|
+
return "\n".join(
|
|
42
|
+
f"• {k}: {v}"
|
|
43
|
+
for k, v in value.items()
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
isinstance(value, Sequence)
|
|
48
|
+
and not isinstance(value, (str, bytes))
|
|
49
|
+
):
|
|
50
|
+
if len(value) == 0:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
return "\n".join(
|
|
54
|
+
f"• {item}"
|
|
55
|
+
for item in value
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return str(value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _add_row(table: Table, label: str, value) -> None:
|
|
62
|
+
"""Skip empty values automatically."""
|
|
63
|
+
|
|
64
|
+
value = _format(value)
|
|
65
|
+
|
|
66
|
+
if value is None:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
table.add_row(label, value)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def display_info(info: DatasetInfo) -> None:
|
|
73
|
+
"""Pretty-print DatasetInfo."""
|
|
74
|
+
|
|
75
|
+
# -------------------------------------------------------------------------
|
|
76
|
+
# Header
|
|
77
|
+
# -------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
console.print(
|
|
80
|
+
Panel.fit(
|
|
81
|
+
Text(info.name, style="title"),
|
|
82
|
+
subtitle=f"[subtitle]{info.provider} • {info.id}[/subtitle]",
|
|
83
|
+
border_style="panel",
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# -------------------------------------------------------------------------
|
|
88
|
+
# General Information
|
|
89
|
+
# -------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
table = Table(
|
|
92
|
+
title="[header]General Information[/header]",
|
|
93
|
+
show_header=False,
|
|
94
|
+
expand=True,
|
|
95
|
+
box=None,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
table.add_column(style="label", width=20)
|
|
99
|
+
table.add_column(style="value")
|
|
100
|
+
|
|
101
|
+
_add_row(table, "Provider", info.provider)
|
|
102
|
+
_add_row(table, "Dataset ID", info.id)
|
|
103
|
+
_add_row(table, "Instances", info.instances)
|
|
104
|
+
_add_row(table, "Features", info.features)
|
|
105
|
+
_add_row(table, "Tasks", info.tasks)
|
|
106
|
+
_add_row(table, "Target", info.target)
|
|
107
|
+
_add_row(table, "Missing Values", info.has_missing_values)
|
|
108
|
+
_add_row(table, "URL", info.url)
|
|
109
|
+
|
|
110
|
+
console.print(table)
|
|
111
|
+
|
|
112
|
+
# -------------------------------------------------------------------------
|
|
113
|
+
# Description
|
|
114
|
+
# -------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
description = _truncate(info.description)
|
|
117
|
+
|
|
118
|
+
if description:
|
|
119
|
+
console.print(
|
|
120
|
+
Panel(
|
|
121
|
+
description,
|
|
122
|
+
title="[description]Description[/description]",
|
|
123
|
+
border_style="description",
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# -------------------------------------------------------------------------
|
|
128
|
+
# Additional Information
|
|
129
|
+
# -------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
if info.extra:
|
|
132
|
+
|
|
133
|
+
extra = Table(
|
|
134
|
+
title="[header]Additional Information[/header]",
|
|
135
|
+
show_header=False,
|
|
136
|
+
expand=True,
|
|
137
|
+
box=None,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
extra.add_column(style="warning", width=25)
|
|
141
|
+
extra.add_column(style="value")
|
|
142
|
+
|
|
143
|
+
for key, value in info.extra.items():
|
|
144
|
+
_add_row(
|
|
145
|
+
extra,
|
|
146
|
+
key.replace("_", " ").title(),
|
|
147
|
+
value,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
console.print(extra)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from textwrap import shorten
|
|
2
|
+
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
from dataflux.display.console import console
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DESCRIPTION_WIDTH = 60
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _truncate(text: str | None) -> str:
|
|
14
|
+
"""Truncate long descriptions cleanly."""
|
|
15
|
+
if not text:
|
|
16
|
+
return "-"
|
|
17
|
+
|
|
18
|
+
return shorten(
|
|
19
|
+
" ".join(text.split()),
|
|
20
|
+
width=DESCRIPTION_WIDTH,
|
|
21
|
+
placeholder="...",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def display_search(results: list, limit: int = 10) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Pretty-print search results using Rich.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
if not results:
|
|
31
|
+
console.print(
|
|
32
|
+
Panel(
|
|
33
|
+
"[error]No datasets found.[/error]",
|
|
34
|
+
title="[header]Search Results[/header]",
|
|
35
|
+
border_style="error",
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# -------------------------------------------------------------------------
|
|
41
|
+
# Single Result
|
|
42
|
+
# -------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
if len(results) == 1:
|
|
45
|
+
result = results[0]
|
|
46
|
+
|
|
47
|
+
text = Text()
|
|
48
|
+
|
|
49
|
+
text.append("Name: ", style="label")
|
|
50
|
+
text.append(f"{result.name}\n", style="value")
|
|
51
|
+
|
|
52
|
+
text.append("Provider: ", style="label")
|
|
53
|
+
text.append(f"{result.provider}\n", style="provider")
|
|
54
|
+
|
|
55
|
+
text.append("ID: ", style="label")
|
|
56
|
+
text.append(f"{result.id}\n", style="value")
|
|
57
|
+
|
|
58
|
+
if result.description:
|
|
59
|
+
text.append("Description: ", style="label")
|
|
60
|
+
text.append(result.description, style="value")
|
|
61
|
+
|
|
62
|
+
if getattr(result, "match_score", None) is not None:
|
|
63
|
+
text.append("\nMatch Score: ", style="label")
|
|
64
|
+
text.append(
|
|
65
|
+
f"{result.match_score:.2f}",
|
|
66
|
+
style="highlight",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
console.print(
|
|
70
|
+
Panel(
|
|
71
|
+
text,
|
|
72
|
+
title="[header]Search Result[/header]",
|
|
73
|
+
border_style="panel",
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# -------------------------------------------------------------------------
|
|
79
|
+
# Multiple Results
|
|
80
|
+
# -------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
display_results = results[:limit]
|
|
83
|
+
|
|
84
|
+
table = Table(
|
|
85
|
+
title=f"[header]Search Results ({len(display_results)} of {len(results)})[/header]",
|
|
86
|
+
show_lines=False,
|
|
87
|
+
expand=True,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
table.add_column("#", justify="right", style="label", no_wrap=True)
|
|
91
|
+
table.add_column("Name", style="title")
|
|
92
|
+
table.add_column("Provider", style="provider")
|
|
93
|
+
table.add_column("ID", style="value")
|
|
94
|
+
table.add_column("Description", style="dim")
|
|
95
|
+
|
|
96
|
+
# Uncomment later if match_score becomes public.
|
|
97
|
+
# table.add_column("Score", justify="right", style="highlight")
|
|
98
|
+
|
|
99
|
+
for index, result in enumerate(display_results, start=1):
|
|
100
|
+
table.add_row(
|
|
101
|
+
str(index),
|
|
102
|
+
result.name,
|
|
103
|
+
result.provider,
|
|
104
|
+
str(result.id),
|
|
105
|
+
_truncate(result.description),
|
|
106
|
+
# f"{result.match_score:.2f}" if result.match_score else "-",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
console.print(table)
|
|
110
|
+
|
|
111
|
+
# -------------------------------------------------------------------------
|
|
112
|
+
# Footer
|
|
113
|
+
# -------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
if len(results) > limit:
|
|
116
|
+
console.print(
|
|
117
|
+
f"[dim]Showing {len(display_results)} of {len(results)} matching datasets.[/dim]"
|
|
118
|
+
)
|
|
119
|
+
console.print(
|
|
120
|
+
"[dim]Use [bold]limit=...[/bold] to display more results or "
|
|
121
|
+
"[bold]display=False[/bold] to suppress console output.[/dim]"
|
|
122
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from rich.theme import Theme
|
|
2
|
+
|
|
3
|
+
DATAFLUX_THEME = Theme(
|
|
4
|
+
{
|
|
5
|
+
# General
|
|
6
|
+
"header": "bold cyan",
|
|
7
|
+
"title": "bold bright_cyan",
|
|
8
|
+
"subtitle": "dim cyan",
|
|
9
|
+
|
|
10
|
+
# Tables
|
|
11
|
+
"label": "bold cyan",
|
|
12
|
+
"value": "white",
|
|
13
|
+
|
|
14
|
+
# Status
|
|
15
|
+
"success": "bold green",
|
|
16
|
+
"warning": "bold yellow",
|
|
17
|
+
"error": "bold red",
|
|
18
|
+
"info": "cyan",
|
|
19
|
+
|
|
20
|
+
# Panels
|
|
21
|
+
"panel": "cyan",
|
|
22
|
+
"description": "green",
|
|
23
|
+
"extra": "yellow",
|
|
24
|
+
|
|
25
|
+
# Search
|
|
26
|
+
"provider": "magenta",
|
|
27
|
+
"highlight": "bold bright_green",
|
|
28
|
+
|
|
29
|
+
# Misc
|
|
30
|
+
"dim": "dim",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from textwrap import fill, shorten
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ============================================================================
|
|
8
|
+
# Constants
|
|
9
|
+
# ============================================================================
|
|
10
|
+
|
|
11
|
+
DESCRIPTION_LIMIT = 600
|
|
12
|
+
DESCRIPTION_WIDTH = 60
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ============================================================================
|
|
16
|
+
# Text Formatting
|
|
17
|
+
# ============================================================================
|
|
18
|
+
|
|
19
|
+
def truncate_description(
|
|
20
|
+
text: str | None,
|
|
21
|
+
*,
|
|
22
|
+
limit: int = DESCRIPTION_LIMIT,
|
|
23
|
+
width: int = 80,
|
|
24
|
+
) -> str | None:
|
|
25
|
+
"""
|
|
26
|
+
Truncate long descriptions for panels.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
if not text:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
text = " ".join(text.split())
|
|
33
|
+
|
|
34
|
+
if len(text) <= limit:
|
|
35
|
+
return fill(text, width=width)
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
fill(text[:limit].rsplit(" ", 1)[0], width=width)
|
|
39
|
+
+ "\n\n[dim]... (description truncated)[/dim]"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def truncate_text(
|
|
44
|
+
text: str | None,
|
|
45
|
+
*,
|
|
46
|
+
width: int = DESCRIPTION_WIDTH,
|
|
47
|
+
) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Truncate text for table cells.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
if not text:
|
|
53
|
+
return "-"
|
|
54
|
+
|
|
55
|
+
return shorten(
|
|
56
|
+
" ".join(text.split()),
|
|
57
|
+
width=width,
|
|
58
|
+
placeholder="...",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ============================================================================
|
|
63
|
+
# Value Formatting
|
|
64
|
+
# ============================================================================
|
|
65
|
+
|
|
66
|
+
def format_value(value):
|
|
67
|
+
"""
|
|
68
|
+
Pretty-format common Python objects for Rich.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
if value is None:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
if isinstance(value, bool):
|
|
75
|
+
return "[success]Yes[/success]" if value else "[error]No[/error]"
|
|
76
|
+
|
|
77
|
+
if isinstance(value, Mapping):
|
|
78
|
+
return "\n".join(
|
|
79
|
+
f"• {k}: {v}"
|
|
80
|
+
for k, v in value.items()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
isinstance(value, Sequence)
|
|
85
|
+
and not isinstance(value, (str, bytes))
|
|
86
|
+
):
|
|
87
|
+
if len(value) == 0:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
return "\n".join(
|
|
91
|
+
f"• {item}"
|
|
92
|
+
for item in value
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return str(value)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def add_row(table, label: str, value) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Add a row to a Rich table while skipping empty values.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
value = format_value(value)
|
|
104
|
+
|
|
105
|
+
if value is None:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
table.add_row(label, value)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ============================================================================
|
|
112
|
+
# Provider Display Names
|
|
113
|
+
# ============================================================================
|
|
114
|
+
|
|
115
|
+
_PROVIDER_NAMES = {
|
|
116
|
+
"uci": "UCI Machine Learning Repository",
|
|
117
|
+
"sklearn": "Scikit-Learn",
|
|
118
|
+
"huggingface": "Hugging Face",
|
|
119
|
+
"kaggle": "Kaggle",
|
|
120
|
+
"torchvision": "TorchVision",
|
|
121
|
+
"pyg": "PyTorch Geometric",
|
|
122
|
+
"seaborn": "Seaborn",
|
|
123
|
+
"statsmodels": "StatsModels",
|
|
124
|
+
"vega": "Vega Datasets",
|
|
125
|
+
"worldbank": "World Bank",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def provider_name(provider: str) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Convert an internal provider identifier into a display name.
|
|
132
|
+
|
|
133
|
+
Examples
|
|
134
|
+
--------
|
|
135
|
+
>>> provider_name("sklearn")
|
|
136
|
+
'Scikit-Learn'
|
|
137
|
+
"""
|
|
138
|
+
return _PROVIDER_NAMES.get(provider.lower(), provider.title())
|