rhythmos-cli 0.1.0__tar.gz → 26.10.4__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.
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/.gitignore +4 -2
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/PKG-INFO +19 -3
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/README.md +15 -1
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/pyproject.toml +19 -2
- rhythmos_cli-26.10.4/src/rhythmos/__init__.py +15 -0
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/src/rhythmos/__main__.py +1 -1
- rhythmos_cli-26.10.4/src/rhythmos/_version.py +24 -0
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/src/rhythmos/cli.py +18 -3
- rhythmos_cli-26.10.4/tests/test_cli.py +118 -0
- rhythmos_cli-0.1.0/src/rhythmos/__init__.py +0 -6
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/LICENCE +0 -0
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/examples/commands/hello.py +0 -0
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/src/rhythmos/base_command.py +0 -0
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/src/rhythmos/discovery.py +0 -0
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/src/rhythmos/stubs/command.py.stub +0 -0
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/src/rhythmos/system_commands/__init__.py +0 -0
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/src/rhythmos/system_commands/list.py +0 -0
- {rhythmos_cli-0.1.0 → rhythmos_cli-26.10.4}/src/rhythmos/system_commands/make.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: rhythmos-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 26.10.4
|
|
4
4
|
Summary: A lightweight CLI framework with project-local auto-discovered commands
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/JoanMbela/rhythmos-cli
|
|
6
6
|
Project-URL: Repository, https://gitlab.com/JoanMbela/rhythmos-cli
|
|
@@ -20,6 +20,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
22
|
Requires-Python: >=3.9
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
23
25
|
Description-Content-Type: text/markdown
|
|
24
26
|
|
|
25
27
|
# Rhythmos CLI
|
|
@@ -74,6 +76,20 @@ See `examples/commands/hello.py` for a ready example.
|
|
|
74
76
|
```bash
|
|
75
77
|
python -m venv .venv
|
|
76
78
|
source .venv/bin/activate
|
|
77
|
-
pip install -e .
|
|
79
|
+
pip install -e ".[dev]"
|
|
78
80
|
RHYTHMOS_COMMANDS=examples/commands rhythmos-cli hello World
|
|
81
|
+
pytest
|
|
79
82
|
```
|
|
83
|
+
|
|
84
|
+
## Release (GitLab CI)
|
|
85
|
+
|
|
86
|
+
Version comes from **git tags** (no manual edits in `pyproject.toml`). Pushing a tag builds and publishes that version:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
git add -A && git commit -m "..."
|
|
90
|
+
git tag v26.10.2
|
|
91
|
+
git push origin v26.10.2
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Requires CI variable `PYPI_TOKEN` (masked + protected) and protected tags `v*`.
|
|
95
|
+
If a tag pipeline fails, push a **new** tag — protected tags usually cannot be deleted via `git`.
|
|
@@ -50,6 +50,20 @@ See `examples/commands/hello.py` for a ready example.
|
|
|
50
50
|
```bash
|
|
51
51
|
python -m venv .venv
|
|
52
52
|
source .venv/bin/activate
|
|
53
|
-
pip install -e .
|
|
53
|
+
pip install -e ".[dev]"
|
|
54
54
|
RHYTHMOS_COMMANDS=examples/commands rhythmos-cli hello World
|
|
55
|
+
pytest
|
|
55
56
|
```
|
|
57
|
+
|
|
58
|
+
## Release (GitLab CI)
|
|
59
|
+
|
|
60
|
+
Version comes from **git tags** (no manual edits in `pyproject.toml`). Pushing a tag builds and publishes that version:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git add -A && git commit -m "..."
|
|
64
|
+
git tag v26.10.2
|
|
65
|
+
git push origin v26.10.2
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Requires CI variable `PYPI_TOKEN` (masked + protected) and protected tags `v*`.
|
|
69
|
+
If a tag pipeline fails, push a **new** tag — protected tags usually cannot be deleted via `git`.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rhythmos-cli"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "A lightweight CLI framework with project-local auto-discovered commands"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -31,10 +31,22 @@ classifiers = [
|
|
|
31
31
|
Homepage = "https://gitlab.com/JoanMbela/rhythmos-cli"
|
|
32
32
|
Repository = "https://gitlab.com/JoanMbela/rhythmos-cli"
|
|
33
33
|
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = ["pytest>=8.0"]
|
|
36
|
+
|
|
34
37
|
[project.scripts]
|
|
35
38
|
rhythmos-cli = "rhythmos.cli:main"
|
|
36
39
|
rhythmos = "rhythmos.cli:main"
|
|
37
40
|
|
|
41
|
+
[tool.hatch.version]
|
|
42
|
+
source = "vcs"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.version.raw-options]
|
|
45
|
+
local_scheme = "no-local-version"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.hooks.vcs]
|
|
48
|
+
version-file = "src/rhythmos/_version.py"
|
|
49
|
+
|
|
38
50
|
[tool.hatch.build.targets.wheel]
|
|
39
51
|
packages = ["src/rhythmos"]
|
|
40
52
|
|
|
@@ -42,6 +54,11 @@ packages = ["src/rhythmos"]
|
|
|
42
54
|
include = [
|
|
43
55
|
"/src",
|
|
44
56
|
"/examples",
|
|
57
|
+
"/tests",
|
|
45
58
|
"/README.md",
|
|
46
59
|
"/LICENCE",
|
|
47
60
|
]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Rhythmos — a lightweight project CLI framework."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from rhythmos.base_command import BaseCommand
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = version("rhythmos-cli")
|
|
9
|
+
except PackageNotFoundError: # pragma: no cover - editable / source tree
|
|
10
|
+
try:
|
|
11
|
+
from rhythmos._version import __version__
|
|
12
|
+
except ImportError:
|
|
13
|
+
__version__ = "0.0.0"
|
|
14
|
+
|
|
15
|
+
__all__ = ["BaseCommand", "__version__"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '26.10.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (26, 10, 4)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -5,10 +5,18 @@ from rhythmos.base_command import BaseCommand
|
|
|
5
5
|
from rhythmos.discovery import commands_dir, discover_commands, invoked_as
|
|
6
6
|
from rhythmos.system_commands import SYSTEM_COMMANDS
|
|
7
7
|
|
|
8
|
+
OFFICIAL_CLIS = frozenset({"rhythmos", "rhythmos-cli"})
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
def resolve_commands(
|
|
12
|
+
include_system_commands: bool = True,
|
|
13
|
+
) -> dict[str, type[BaseCommand]]:
|
|
10
14
|
"""Merge system + user commands. User commands win on name clashes."""
|
|
11
15
|
user = discover_commands()
|
|
16
|
+
|
|
17
|
+
if not include_system_commands:
|
|
18
|
+
return {**user}
|
|
19
|
+
|
|
12
20
|
for name in user:
|
|
13
21
|
if name in SYSTEM_COMMANDS:
|
|
14
22
|
print(
|
|
@@ -18,16 +26,23 @@ def resolve_commands() -> dict[str, type[BaseCommand]]:
|
|
|
18
26
|
return {**SYSTEM_COMMANDS, **user}
|
|
19
27
|
|
|
20
28
|
|
|
21
|
-
def main(
|
|
29
|
+
def main(
|
|
30
|
+
argv: list[str] | None = None,
|
|
31
|
+
*,
|
|
32
|
+
include_system_commands: bool | None = None,
|
|
33
|
+
) -> None:
|
|
22
34
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
23
35
|
cli = invoked_as()
|
|
36
|
+
if include_system_commands is None:
|
|
37
|
+
# Official entry points expose make/list; project wrappers do not by default.
|
|
38
|
+
include_system_commands = cli in OFFICIAL_CLIS
|
|
24
39
|
|
|
25
40
|
parser = argparse.ArgumentParser(prog=cli, description="Rhythmos CLI")
|
|
26
41
|
subparsers = parser.add_subparsers(
|
|
27
42
|
dest="command", help="Command to execute", required=True
|
|
28
43
|
)
|
|
29
44
|
|
|
30
|
-
commands = resolve_commands()
|
|
45
|
+
commands = resolve_commands(include_system_commands)
|
|
31
46
|
command_instances = {}
|
|
32
47
|
|
|
33
48
|
for command_name, command_class in commands.items():
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from rhythmos.base_command import BaseCommand
|
|
4
|
+
from rhythmos.cli import main, resolve_commands
|
|
5
|
+
from rhythmos.discovery import (
|
|
6
|
+
COMMANDS_DIR_ENV,
|
|
7
|
+
discover_commands,
|
|
8
|
+
scaffold_command,
|
|
9
|
+
to_class_name,
|
|
10
|
+
to_snake,
|
|
11
|
+
)
|
|
12
|
+
from rhythmos.system_commands import SYSTEM_COMMANDS
|
|
13
|
+
from rhythmos.system_commands.list import ListCommand
|
|
14
|
+
from rhythmos.system_commands.make import MakeCommand
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
HELLO_CMD = """\
|
|
18
|
+
from rhythmos import BaseCommand
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HelloCommand(BaseCommand):
|
|
22
|
+
def get_description(self):
|
|
23
|
+
return "Say hello"
|
|
24
|
+
|
|
25
|
+
def add_arguments(self):
|
|
26
|
+
self.parser.add_argument("name")
|
|
27
|
+
|
|
28
|
+
def handle(self, name):
|
|
29
|
+
print(f"Hello, {name}!")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def commands_home(tmp_path, monkeypatch):
|
|
35
|
+
directory = tmp_path / "commands"
|
|
36
|
+
directory.mkdir()
|
|
37
|
+
monkeypatch.setenv(COMMANDS_DIR_ENV, str(directory))
|
|
38
|
+
return directory
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_to_snake_and_class_name():
|
|
42
|
+
assert to_snake("HelloWorld") == "hello_world"
|
|
43
|
+
assert to_snake("foo-bar") == "foo_bar"
|
|
44
|
+
assert to_class_name("foo_bar") == "FooBar"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_discover_commands_loads_user_modules(commands_home):
|
|
48
|
+
(commands_home / "hello.py").write_text(HELLO_CMD, encoding="utf-8")
|
|
49
|
+
found = discover_commands()
|
|
50
|
+
assert set(found) == {"hello"}
|
|
51
|
+
assert issubclass(found["hello"], BaseCommand)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_resolve_commands_excludes_system_when_disabled(commands_home):
|
|
55
|
+
(commands_home / "hello.py").write_text(HELLO_CMD, encoding="utf-8")
|
|
56
|
+
resolved = resolve_commands(include_system_commands=False)
|
|
57
|
+
assert set(resolved) == {"hello"}
|
|
58
|
+
assert "list" not in resolved
|
|
59
|
+
assert "make" not in resolved
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_resolve_commands_includes_system_when_enabled(commands_home):
|
|
63
|
+
(commands_home / "hello.py").write_text(HELLO_CMD, encoding="utf-8")
|
|
64
|
+
resolved = resolve_commands(include_system_commands=True)
|
|
65
|
+
assert set(resolved) == {"hello", "list", "make"}
|
|
66
|
+
assert resolved["list"] is ListCommand
|
|
67
|
+
assert resolved["make"] is MakeCommand
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_resolve_commands_user_overrides_system(commands_home, capsys):
|
|
71
|
+
(commands_home / "list.py").write_text(
|
|
72
|
+
"""\
|
|
73
|
+
from rhythmos import BaseCommand
|
|
74
|
+
|
|
75
|
+
class ListCommand(BaseCommand):
|
|
76
|
+
def get_description(self):
|
|
77
|
+
return "user list"
|
|
78
|
+
|
|
79
|
+
def handle(self):
|
|
80
|
+
print("user")
|
|
81
|
+
""",
|
|
82
|
+
encoding="utf-8",
|
|
83
|
+
)
|
|
84
|
+
resolved = resolve_commands(include_system_commands=True)
|
|
85
|
+
assert resolved["list"] is not ListCommand
|
|
86
|
+
assert resolved["list"] is not SYSTEM_COMMANDS["list"]
|
|
87
|
+
err = capsys.readouterr().err
|
|
88
|
+
assert "overrides system command 'list'" in err
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_scaffold_command_writes_stub(commands_home):
|
|
92
|
+
path = scaffold_command("greet", "Greet someone")
|
|
93
|
+
assert path == commands_home / "greet.py"
|
|
94
|
+
text = path.read_text(encoding="utf-8")
|
|
95
|
+
assert "class GreetCommand" in text
|
|
96
|
+
assert "Greet someone" in text
|
|
97
|
+
assert "from rhythmos import BaseCommand" in text
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_scaffold_command_rejects_duplicates(commands_home):
|
|
101
|
+
scaffold_command("greet")
|
|
102
|
+
with pytest.raises(FileExistsError):
|
|
103
|
+
scaffold_command("greet")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_main_with_system_commands_runs_list(commands_home, capsys):
|
|
107
|
+
(commands_home / "hello.py").write_text(HELLO_CMD, encoding="utf-8")
|
|
108
|
+
main(["list"], include_system_commands=True)
|
|
109
|
+
out = capsys.readouterr().out
|
|
110
|
+
assert "hello" in out
|
|
111
|
+
assert "[user" in out
|
|
112
|
+
assert "[system]" in out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_main_without_system_commands_rejects_list(commands_home):
|
|
116
|
+
(commands_home / "hello.py").write_text(HELLO_CMD, encoding="utf-8")
|
|
117
|
+
with pytest.raises(SystemExit):
|
|
118
|
+
main(["list"], include_system_commands=False)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|