datbox-core 0.0.1__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,19 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .coverage
11
+ htmlcov/
12
+ *.duckdb
13
+ *.sqlite
14
+ .env
15
+ .env.*
16
+ !.env.example
17
+ .idea/
18
+ .vscode/
19
+ .DS_Store
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.5
2
+ Name: datbox-core
3
+ Version: 0.0.1
4
+ Summary: Core of the Data Analyst Toolbox: the `dat` CLI, plugin registry, config, secrets, and output writers.
5
+ Project-URL: Homepage, https://github.com/nick-hebert/datbox
6
+ Project-URL: Repository, https://github.com/nick-hebert/datbox
7
+ Project-URL: Issues, https://github.com/nick-hebert/datbox/issues
8
+ Author: Nick Hebert
9
+ License-Expression: Apache-2.0
10
+ Keywords: analytics,cli,data,toolbox
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: pydantic>=2.7
21
+ Requires-Dist: typer>=0.12
22
+ Description-Content-Type: text/markdown
23
+
24
+ # datbox-core
25
+
26
+ The core of **dat**, the Data Analyst Toolbox. Provides the `dat` command-line entry point
27
+ and the plugin registry that other `datbox-*` packages register their subcommands with.
28
+
29
+ ```bash
30
+ pip install datbox-core
31
+ dat --help
32
+ ```
33
+
34
+ Other modules (`datbox-viz`, `datbox-fake`, ...) add subcommands automatically when installed.
@@ -0,0 +1,11 @@
1
+ # datbox-core
2
+
3
+ The core of **dat**, the Data Analyst Toolbox. Provides the `dat` command-line entry point
4
+ and the plugin registry that other `datbox-*` packages register their subcommands with.
5
+
6
+ ```bash
7
+ pip install datbox-core
8
+ dat --help
9
+ ```
10
+
11
+ Other modules (`datbox-viz`, `datbox-fake`, ...) add subcommands automatically when installed.
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "datbox-core"
3
+ version = "0.0.1"
4
+ description = "Core of the Data Analyst Toolbox: the `dat` CLI, plugin registry, config, secrets, and output writers."
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.11"
8
+ authors = [{ name = "Nick Hebert" }]
9
+ keywords = ["data", "analytics", "cli", "toolbox"]
10
+ classifiers = [
11
+ "Development Status :: 2 - Pre-Alpha",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Topic :: Scientific/Engineering :: Information Analysis",
19
+ ]
20
+ dependencies = [
21
+ "typer>=0.12",
22
+ "pydantic>=2.7",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/nick-hebert/datbox"
27
+ Repository = "https://github.com/nick-hebert/datbox"
28
+ Issues = "https://github.com/nick-hebert/datbox/issues"
29
+
30
+ [project.scripts]
31
+ dat = "datbox_core.cli:main"
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/datbox_core"]
@@ -0,0 +1,10 @@
1
+ """datbox-core: the `dat` CLI, plugin registry, and shared foundations."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("datbox-core")
7
+ except PackageNotFoundError: # pragma: no cover - source checkout without install
8
+ __version__ = "0.0.0"
9
+
10
+ __all__ = ["__version__"]
@@ -0,0 +1,68 @@
1
+ """The `dat` root command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from datbox_core import __version__
8
+ from datbox_core.plugins import discover
9
+
10
+ app = typer.Typer(
11
+ name="dat",
12
+ help="dat: the Data Analyst Toolbox. Portable, zero-infrastructure utilities for analysts.",
13
+ no_args_is_help=True,
14
+ add_completion=False,
15
+ )
16
+
17
+
18
+ def _version_callback(value: bool) -> None:
19
+ if value:
20
+ typer.echo(f"dat {__version__}")
21
+ raise typer.Exit()
22
+
23
+
24
+ @app.callback()
25
+ def root(
26
+ version: bool = typer.Option(
27
+ False,
28
+ "--version",
29
+ "-V",
30
+ help="Show the dat version and exit.",
31
+ callback=_version_callback,
32
+ is_eager=True,
33
+ ),
34
+ ) -> None:
35
+ """Root callback; subcommands are supplied by installed datbox-* plugins."""
36
+
37
+
38
+ @app.command()
39
+ def plugins() -> None:
40
+ """List installed dat modules and report any that failed to load."""
41
+ loaded, failed = discover()
42
+ if not loaded and not failed:
43
+ typer.echo("No plugins installed. Try: pip install datbox-viz")
44
+ return
45
+ for plugin in loaded:
46
+ dist = f" ({plugin.distribution})" if plugin.distribution else ""
47
+ typer.echo(f" {plugin.name}{dist}")
48
+ for broken in failed:
49
+ typer.secho(f" {broken.name} FAILED: {broken.error}", fg=typer.colors.RED, err=True)
50
+ if failed:
51
+ raise typer.Exit(code=1)
52
+
53
+
54
+ def build_app() -> typer.Typer:
55
+ """Attach every discovered plugin to the root app and return it."""
56
+ loaded, _failed = discover()
57
+ for plugin in loaded:
58
+ app.add_typer(plugin.app, name=plugin.name)
59
+ return app
60
+
61
+
62
+ def main() -> None:
63
+ """Console-script entry point."""
64
+ build_app()()
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
@@ -0,0 +1,67 @@
1
+ """Plugin discovery for `dat` subcommands.
2
+
3
+ Any installed distribution can register a Typer app under the ``datbox.plugins``
4
+ entry-point group. The entry-point *name* becomes the subcommand name and the
5
+ *value* must resolve to a ``typer.Typer`` instance.
6
+
7
+ Example ``pyproject.toml`` for a module::
8
+
9
+ [project.entry-points."datbox.plugins"]
10
+ viz = "datbox_viz.cli:app"
11
+
12
+ Modules never import each other; they only depend on ``datbox-core``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from importlib.metadata import EntryPoint, entry_points
19
+
20
+ import typer
21
+
22
+ PLUGIN_GROUP = "datbox.plugins"
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class LoadedPlugin:
27
+ """A successfully loaded plugin subcommand."""
28
+
29
+ name: str
30
+ app: typer.Typer
31
+ distribution: str | None
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class FailedPlugin:
36
+ """A plugin that was found but could not be loaded."""
37
+
38
+ name: str
39
+ error: str
40
+
41
+
42
+ def discover() -> tuple[list[LoadedPlugin], list[FailedPlugin]]:
43
+ """Find and load every registered plugin.
44
+
45
+ A plugin that fails to import never breaks the root CLI; it is reported
46
+ instead so the user can see what is broken.
47
+ """
48
+ loaded: list[LoadedPlugin] = []
49
+ failed: list[FailedPlugin] = []
50
+ for ep in _iter_entry_points():
51
+ try:
52
+ obj = ep.load()
53
+ except Exception as exc:
54
+ failed.append(FailedPlugin(name=ep.name, error=f"{type(exc).__name__}: {exc}"))
55
+ continue
56
+ if not isinstance(obj, typer.Typer):
57
+ failed.append(
58
+ FailedPlugin(name=ep.name, error=f"entry point is {type(obj).__name__}, not Typer")
59
+ )
60
+ continue
61
+ dist = ep.dist.name if ep.dist is not None else None
62
+ loaded.append(LoadedPlugin(name=ep.name, app=obj, distribution=dist))
63
+ return loaded, failed
64
+
65
+
66
+ def _iter_entry_points() -> list[EntryPoint]:
67
+ return sorted(entry_points(group=PLUGIN_GROUP), key=lambda ep: ep.name)
File without changes
File without changes
@@ -0,0 +1,24 @@
1
+ from typer.testing import CliRunner
2
+
3
+ from datbox_core import __version__
4
+ from datbox_core.cli import build_app
5
+
6
+ runner = CliRunner()
7
+
8
+
9
+ def test_help_runs() -> None:
10
+ result = runner.invoke(build_app(), ["--help"])
11
+ assert result.exit_code == 0
12
+ assert "Data Analyst Toolbox" in result.output
13
+
14
+
15
+ def test_version_flag() -> None:
16
+ result = runner.invoke(build_app(), ["--version"])
17
+ assert result.exit_code == 0
18
+ assert __version__ in result.output
19
+
20
+
21
+ def test_plugins_command_with_none_installed() -> None:
22
+ result = runner.invoke(build_app(), ["plugins"])
23
+ assert result.exit_code == 0
24
+ assert "No plugins installed" in result.output
@@ -0,0 +1,33 @@
1
+ from importlib.metadata import EntryPoint
2
+
3
+ import typer
4
+
5
+ from datbox_core import plugins
6
+
7
+
8
+ def test_discover_loads_valid_typer_app(monkeypatch) -> None: # type: ignore[no-untyped-def]
9
+ fake_app = typer.Typer()
10
+
11
+ class FakeEP(EntryPoint):
12
+ def load(self): # type: ignore[override]
13
+ return fake_app
14
+
15
+ ep = FakeEP(name="fake", value="x:y", group=plugins.PLUGIN_GROUP)
16
+ monkeypatch.setattr(plugins, "_iter_entry_points", lambda: [ep])
17
+ loaded, failed = plugins.discover()
18
+ assert [p.name for p in loaded] == ["fake"]
19
+ assert loaded[0].app is fake_app
20
+ assert failed == []
21
+
22
+
23
+ def test_discover_reports_broken_plugin(monkeypatch) -> None: # type: ignore[no-untyped-def]
24
+ class BrokenEP(EntryPoint):
25
+ def load(self): # type: ignore[override]
26
+ raise ImportError("boom")
27
+
28
+ ep = BrokenEP(name="broken", value="x:y", group=plugins.PLUGIN_GROUP)
29
+ monkeypatch.setattr(plugins, "_iter_entry_points", lambda: [ep])
30
+ loaded, failed = plugins.discover()
31
+ assert loaded == []
32
+ assert failed[0].name == "broken"
33
+ assert "ImportError" in failed[0].error