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.
Files changed (69) hide show
  1. dataflux/__init__.py +4 -0
  2. dataflux/__version__.py +1 -0
  3. dataflux/cache.py +122 -0
  4. dataflux/config.py +18 -0
  5. dataflux/display/__init__.py +0 -0
  6. dataflux/display/console.py +4 -0
  7. dataflux/display/info.py +150 -0
  8. dataflux/display/search.py +122 -0
  9. dataflux/display/theme.py +32 -0
  10. dataflux/display/utils.py +138 -0
  11. dataflux/exceptions.py +150 -0
  12. dataflux/export.py +62 -0
  13. dataflux/flux.py +55 -0
  14. dataflux/models/__init__.py +0 -0
  15. dataflux/models/dataset.py +15 -0
  16. dataflux/models/search_result.py +10 -0
  17. dataflux/providers/__init__.py +0 -0
  18. dataflux/providers/base.py +23 -0
  19. dataflux/providers/huggingface.py +91 -0
  20. dataflux/providers/kaggle.py +85 -0
  21. dataflux/providers/seaborn.py +66 -0
  22. dataflux/providers/sklearn.py +67 -0
  23. dataflux/providers/statsmodels.py +66 -0
  24. dataflux/providers/torchvision.py +80 -0
  25. dataflux/providers/uci.py +78 -0
  26. dataflux/providers/vega_datasets.py +64 -0
  27. dataflux/providers/worldbank.py +73 -0
  28. dataflux/registry.py +38 -0
  29. dataflux/resolver.py +30 -0
  30. dataflux/resources/kaggle_index.json +365901 -0
  31. dataflux/resources/seaborn_dataset.py +24 -0
  32. dataflux/resources/sklearn_dataset.py +17 -0
  33. dataflux/resources/statsmodels_dataset.py +30 -0
  34. dataflux/resources/torch_vision_dataset.py +89 -0
  35. dataflux/tests/conftest.py +33 -0
  36. dataflux/tests/display/test_info.py +159 -0
  37. dataflux/tests/display/test_search.py +83 -0
  38. dataflux/tests/models/test_dataset_info.py +46 -0
  39. dataflux/tests/models/test_search_result.py +28 -0
  40. dataflux/tests/providers/test_hugging.py +93 -0
  41. dataflux/tests/providers/test_kaggle.py +94 -0
  42. dataflux/tests/providers/test_seaborn.py +88 -0
  43. dataflux/tests/providers/test_sklearn.py +92 -0
  44. dataflux/tests/providers/test_statsmodels.py +89 -0
  45. dataflux/tests/providers/test_torchvision.py +90 -0
  46. dataflux/tests/providers/test_uci.py +92 -0
  47. dataflux/tests/providers/test_vega.py +90 -0
  48. dataflux/tests/providers/test_worldbank.py +111 -0
  49. dataflux/tests/resources/test_resources.py +80 -0
  50. dataflux/tests/test_exceptions.py +20 -0
  51. dataflux/tests/test_export.py +146 -0
  52. dataflux/tests/test_flux.py +40 -0
  53. dataflux/tests/test_models.py +30 -0
  54. dataflux/tests/test_registry.py +149 -0
  55. dataflux/tests/test_resolver.py +77 -0
  56. dataflux/tests/utils/test_cache.py +173 -0
  57. dataflux/tests/utils/test_download.py +135 -0
  58. dataflux/tests/utils/test_filesystem.py +128 -0
  59. dataflux/tests/utils/test_fingerprint.py +114 -0
  60. dataflux/tests/utils/test_search_utils.py +152 -0
  61. dataflux/utils/__init__.py +0 -0
  62. dataflux/utils/download.py +110 -0
  63. dataflux/utils/filesystem.py +89 -0
  64. dataflux/utils/fingerprint.py +155 -0
  65. dataflux/utils/search.py +66 -0
  66. dataflux_core-0.1.0.dist-info/METADATA +400 -0
  67. dataflux_core-0.1.0.dist-info/RECORD +69 -0
  68. dataflux_core-0.1.0.dist-info/WHEEL +4 -0
  69. dataflux_core-0.1.0.dist-info/licenses/LICENSE +21 -0
dataflux/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .__version__ import __version__ as version
2
+ from .flux import flux
3
+
4
+ __all__ = ("version", "flux")
@@ -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
@@ -0,0 +1,4 @@
1
+ from rich.console import Console
2
+ from dataflux.display.theme import DATAFLUX_THEME
3
+
4
+ console = Console(theme=DATAFLUX_THEME)
@@ -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())