paces 0.0.6__tar.gz → 0.0.7__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.
Files changed (36) hide show
  1. {paces-0.0.6 → paces-0.0.7}/PKG-INFO +3 -3
  2. paces-0.0.7/paces/__main__.py +39 -0
  3. {paces-0.0.6 → paces-0.0.7}/paces/tools.py +1 -1
  4. {paces-0.0.6 → paces-0.0.7}/pyproject.toml +4 -4
  5. paces-0.0.7/tests/test_cli_surface.py +133 -0
  6. paces-0.0.6/paces/__main__.py +0 -31
  7. {paces-0.0.6 → paces-0.0.7}/.gitignore +0 -0
  8. {paces-0.0.6 → paces-0.0.7}/LICENSE +0 -0
  9. {paces-0.0.6 → paces-0.0.7}/README.md +0 -0
  10. {paces-0.0.6 → paces-0.0.7}/docs/README.md +0 -0
  11. {paces-0.0.6 → paces-0.0.7}/docs/alignment/README.md +0 -0
  12. {paces-0.0.6 → paces-0.0.7}/docs/poc-reference/README.md +0 -0
  13. {paces-0.0.6 → paces-0.0.7}/docs/poc-reference/artifacts/clips.json +0 -0
  14. {paces-0.0.6 → paces-0.0.7}/paces/__init__.py +0 -0
  15. {paces-0.0.6 → paces-0.0.7}/paces/derivation.py +0 -0
  16. {paces-0.0.6 → paces-0.0.7}/paces/edits.py +0 -0
  17. {paces-0.0.6 → paces-0.0.7}/paces/excerpts.py +0 -0
  18. {paces-0.0.6 → paces-0.0.7}/paces/measure.py +0 -0
  19. {paces-0.0.6 → paces-0.0.7}/paces/model.py +0 -0
  20. {paces-0.0.6 → paces-0.0.7}/paces/projection.py +0 -0
  21. {paces-0.0.6 → paces-0.0.7}/paces/render.py +0 -0
  22. {paces-0.0.6 → paces-0.0.7}/paces/segmenters.py +0 -0
  23. {paces-0.0.6 → paces-0.0.7}/tests/audio_synth.py +0 -0
  24. {paces-0.0.6 → paces-0.0.7}/tests/data/routine.json +0 -0
  25. {paces-0.0.6 → paces-0.0.7}/tests/poc_fixture.py +0 -0
  26. {paces-0.0.6 → paces-0.0.7}/tests/test_chapters.py +0 -0
  27. {paces-0.0.6 → paces-0.0.7}/tests/test_derivation.py +0 -0
  28. {paces-0.0.6 → paces-0.0.7}/tests/test_edits.py +0 -0
  29. {paces-0.0.6 → paces-0.0.7}/tests/test_excerpts.py +0 -0
  30. {paces-0.0.6 → paces-0.0.7}/tests/test_measure.py +0 -0
  31. {paces-0.0.6 → paces-0.0.7}/tests/test_render_media.py +0 -0
  32. {paces-0.0.6 → paces-0.0.7}/tests/test_roundtrip_poc.py +0 -0
  33. {paces-0.0.6 → paces-0.0.7}/tests/test_segment.py +0 -0
  34. {paces-0.0.6 → paces-0.0.7}/tests/test_smoke.py +0 -0
  35. {paces-0.0.6 → paces-0.0.7}/tests/test_vertical_slice.py +0 -0
  36. {paces-0.0.6 → paces-0.0.7}/tests/video_synth.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: paces
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Turn instructional media into structured, interactive learning material
5
5
  Project-URL: Homepage, https://github.com/thorwhalen/paces
6
6
  Project-URL: Repository, https://github.com/thorwhalen/paces
@@ -17,9 +17,9 @@ Requires-Dist: mixing[audio,beats]>=0.0.36; extra == 'audio'
17
17
  Requires-Dist: numba>=0.59; extra == 'audio'
18
18
  Provides-Extra: cli
19
19
  Requires-Dist: argcomplete>=3; extra == 'cli'
20
- Requires-Dist: argh>=0.30; extra == 'cli'
20
+ Requires-Dist: cw<0.2,>=0.1.1; extra == 'cli'
21
21
  Provides-Extra: dev
22
- Requires-Dist: argh>=0.30; extra == 'dev'
22
+ Requires-Dist: cw<0.2,>=0.1.1; extra == 'dev'
23
23
  Requires-Dist: mixing[audio,beats]>=0.0.39; extra == 'dev'
24
24
  Requires-Dist: numba>=0.59; extra == 'dev'
25
25
  Requires-Dist: pytest-cov>=4.0; extra == 'dev'
@@ -0,0 +1,39 @@
1
+ # PYTHON_ARGCOMPLETE_OK
2
+ """paces CLI entry point — ``python -m paces`` / ``paces`` (after install)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import dataclasses
7
+
8
+ import cw
9
+
10
+ from paces.tools import _dispatch_funcs
11
+
12
+ #: Positional args (with or without defaults) stay positional; the keyword-only args
13
+ #: every tool uses become ``--flags``.
14
+ #:
15
+ #: This is ``argh``'s post-0.30 ``BY_NAME_IF_KWONLY`` policy, which this CLI has always
16
+ #: run on and which cw spells the same way. It is **not** cw's default: ``cw.ARGH``
17
+ #: reproduces ``argh.dispatch_commands`` instead, under which a defaulted positional
18
+ #: becomes an option. ``segment(media=None, *, ...)`` is exactly that shape, so the
19
+ #: difference is the difference between ``paces segment video.mp4`` and
20
+ #: ``paces segment --media video.mp4``. The former is what the docs, the smoke test and
21
+ #: the vertical-slice test all type.
22
+ CONVENTION = dataclasses.replace(cw.ARGH, naming=cw.BY_NAME_IF_KWONLY)
23
+
24
+
25
+ def main() -> None:
26
+ """Build the parser over the paces tools and dispatch.
27
+
28
+ ``cw.run`` offers the parser to ``argcomplete`` before parsing, exactly where
29
+ ``argh``'s dispatch did, so ``PYTHON_ARGCOMPLETE_OK`` above still applies with no
30
+ hand-written completion block here. It *returns* the exit code rather than raising
31
+ it, which is why the ``SystemExit`` is written out: without it every usage error
32
+ would exit 0, and the end-to-end tests all assert ``returncode == 0``.
33
+ """
34
+ parser = cw.mk_parser(_dispatch_funcs, convention=CONVENTION)
35
+ raise SystemExit(cw.run(parser))
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -1,7 +1,7 @@
1
1
  """Agent-callable tool surface over ``paces`` — plain functions with JSON-able
2
2
  arguments returning JSON-ready values, deliberately CLI/MCP/HTTP-agnostic.
3
3
 
4
- This module is the SSOT for "what paces can do": every surface (the ``argh``
4
+ This module is the SSOT for "what paces can do": every surface (the ``cw``
5
5
  CLI in ``__main__.py`` today; ``py2mcp``/``qh`` wrappers tomorrow) dispatches
6
6
  over ``_dispatch_funcs`` and nothing else. Arguments that carry structure
7
7
  (steps, grids, documents) accept a Python object, a JSON string, or a path to
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "paces"
9
- version = "0.0.6"
9
+ version = "0.0.7"
10
10
  description = "Turn instructional media into structured, interactive learning material"
11
11
  readme = "README.md"
12
12
  license = "MIT"
@@ -36,7 +36,7 @@ Documentation = "https://thorwhalen.github.io/paces"
36
36
  paces = "paces.__main__:main"
37
37
 
38
38
  [project.optional-dependencies]
39
- cli = ["argh>=0.30", "argcomplete>=3"]
39
+ cli = ["cw>=0.1.1,<0.2", "argcomplete>=3"]
40
40
  audio = [
41
41
  # Grid measurement (issue #2): the fleet's `mixing` owns speech/music
42
42
  # segmentation and beat tracking; [beats] pulls librosa, [audio] pulls
@@ -63,9 +63,9 @@ dev = [
63
63
  "pytest>=7.0",
64
64
  "pytest-cov>=4.0",
65
65
  "ruff>=0.1.0",
66
- # The CLI smoke test runs `python -m paces ...`; without argh in dev it
66
+ # The CLI smoke test runs `python -m paces ...`; without cw in dev it
67
67
  # would fail rather than silently skip — keep it mirrored with [cli].
68
- "argh>=0.30",
68
+ "cw>=0.1.1,<0.2",
69
69
  # The grid-measurement tests exercise the real mixing/librosa path on
70
70
  # synthesized audio; mirrored with [audio] so they run rather than skip.
71
71
  # The derivation tests do the same on synthetic video — mirrored with
@@ -0,0 +1,133 @@
1
+ """The command line itself: which argv spellings `paces` accepts, and what they exit.
2
+
3
+ ``tests/test_smoke.py`` and ``tests/test_vertical_slice.py`` already drive the CLI
4
+ end to end — but both only ever pass argv that works, and both assert
5
+ ``returncode == 0``, so a CLI that had stopped reporting failure at all would sail
6
+ through them. This file pins the shape of the surface instead: the naming policy in
7
+ both directions, and the exit codes.
8
+
9
+ Recorded from the ``argh`` implementation before the ``cw`` migration and replayed
10
+ after: 20 argv vectors compared byte-for-byte on stdout, stderr and exit code, plus
11
+ four vectors whose stderr carries a traceback (compared on exit code and the
12
+ exception's own line, since a traceback names line numbers that the edit moved).
13
+ No diffs. That full-body diff cannot live in CI, because CPython rewrites argparse's
14
+ own option column between versions, so what is asserted here is the grammar.
15
+
16
+ **The naming policy is the load-bearing part.** `paces` runs on argh's post-0.30
17
+ ``BY_NAME_IF_KWONLY``: positional parameters stay positional, keyword-only parameters
18
+ become ``--flags``. That is *not* cw's default — ``cw.ARGH`` reproduces
19
+ ``argh.dispatch_commands``, under which a parameter with a default becomes an option.
20
+ ``segment(media=None, *, ...)`` is exactly that shape, so under the default
21
+ ``paces segment video.mp4`` would become ``paces segment --media video.mp4``: the
22
+ form every doc, the smoke test and the vertical slice actually type would stop
23
+ parsing. So both halves are asserted — the positional is accepted AND the option
24
+ spelling is rejected.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import os
30
+ import subprocess
31
+ import sys
32
+
33
+ import pytest
34
+
35
+ from paces.__main__ import CONVENTION
36
+ from paces.tools import _dispatch_funcs
37
+
38
+ _CLI_TIMEOUT = 120
39
+
40
+ #: Every verb the CLI exposes, spelled as the command line spells it.
41
+ COMMAND_NAMES = [f.__name__.replace("_", "-") for f in _dispatch_funcs]
42
+
43
+
44
+ def run_cli(*argv, cwd):
45
+ """Run ``python -m paces <argv>``, which is how both existing CLI tests enter."""
46
+ return subprocess.run(
47
+ [sys.executable, "-m", "paces", *argv],
48
+ cwd=str(cwd),
49
+ capture_output=True,
50
+ text=True,
51
+ timeout=_CLI_TIMEOUT,
52
+ env={**os.environ, "COLUMNS": "80"},
53
+ )
54
+
55
+
56
+ def test_every_tool_is_reachable_and_documents_itself(tmp_path):
57
+ """``_dispatch_funcs`` is the SSOT; every name in it is a subcommand."""
58
+ top = run_cli("--help", cwd=tmp_path)
59
+ assert top.returncode == 0
60
+ for name in COMMAND_NAMES:
61
+ assert name in top.stdout, f"{name} missing from `paces --help`"
62
+ sub = run_cli(name, "--help", cwd=tmp_path)
63
+ assert sub.returncode == 0, sub.stderr
64
+ assert "usage: " in sub.stdout
65
+
66
+
67
+ def test_media_is_a_positional_not_an_option(tmp_path):
68
+ """The naming policy, in both directions. See the module docstring."""
69
+ help_ = run_cli("segment", "--help", cwd=tmp_path)
70
+ assert "[media]" in help_.stdout
71
+ assert "--media" not in help_.stdout
72
+
73
+ # Accepted as a bare word — the form the smoke test and the docs type.
74
+ positional = run_cli("segment", "media.mp4", cwd=tmp_path)
75
+ assert positional.returncode == 0
76
+
77
+ # And the option spelling does not exist. Under cw's default convention this
78
+ # would be the *only* spelling that worked.
79
+ optional = run_cli("segment", "--media", "media.mp4", cwd=tmp_path)
80
+ assert optional.returncode == 2
81
+
82
+
83
+ def test_keyword_only_parameters_are_flags(tmp_path):
84
+ """The other half of BY_NAME_IF_KWONLY: every kwonly arg is a ``--flag``."""
85
+ help_ = run_cli("to-document", "--help", cwd=tmp_path)
86
+ for flag in ("--doc-id", "--title", "--source", "--domain", "--lang", "--output"):
87
+ assert flag in help_.stdout
88
+ # ...and the one positional parameter is still a bare word in the usage line.
89
+ usage = help_.stdout.split("\n\n", 1)[0]
90
+ assert "segmentation" in usage
91
+ assert "--segmentation" not in help_.stdout
92
+
93
+
94
+ def test_the_convention_is_the_one_the_cli_was_built_on():
95
+ """A guard on the constant itself, so a refactor cannot quietly drop it."""
96
+ import cw
97
+
98
+ assert CONVENTION.naming is cw.BY_NAME_IF_KWONLY
99
+ assert CONVENTION.naming is not cw.ARGH.naming
100
+
101
+
102
+ @pytest.mark.parametrize(
103
+ "argv",
104
+ [
105
+ ("no-such-command",),
106
+ ("segment", "--no-such-flag"),
107
+ ("render",), # a required positional, omitted
108
+ ("segment", "extra1", "extra2"),
109
+ ("edit", "doc"), # `edits` and the required --by are both missing
110
+ ],
111
+ )
112
+ def test_usage_errors_exit_two(argv, tmp_path):
113
+ """``cw.run`` returns the code, so ``main`` must ``raise SystemExit`` on it.
114
+
115
+ Both existing end-to-end CLI tests assert ``returncode == 0``; without this,
116
+ a CLI that had stopped reporting failure entirely would still pass them.
117
+ """
118
+ assert run_cli(*argv, cwd=tmp_path).returncode == 2
119
+
120
+
121
+ def test_a_failing_command_exits_nonzero(tmp_path):
122
+ """An unknown segmenter is a real failure, and the shell has to hear about it."""
123
+ result = run_cli("segment", "--segmenter", "nope", cwd=tmp_path)
124
+ assert result.returncode != 0
125
+ assert "unknown segmenter" in result.stderr
126
+
127
+
128
+ def test_no_arguments_prints_usage_to_stdout_and_exits_zero(tmp_path):
129
+ """argh's behaviour; plain argparse with a required subparser does NOT do this."""
130
+ result = run_cli(cwd=tmp_path)
131
+ assert result.returncode == 0
132
+ assert result.stdout.startswith("usage: ")
133
+ assert result.stderr == ""
@@ -1,31 +0,0 @@
1
- # PYTHON_ARGCOMPLETE_OK
2
- """paces CLI entry point — ``python -m paces`` / ``paces`` (after install)."""
3
-
4
- from __future__ import annotations
5
-
6
- import argh
7
-
8
- from paces.tools import _dispatch_funcs
9
-
10
-
11
- def main() -> None:
12
- """Build the argh parser over the paces tools and dispatch."""
13
- from argh.assembling import NameMappingPolicy
14
-
15
- parser = argh.ArghParser()
16
- # Positional args (with or without defaults) stay positional; the
17
- # keyword-only args every tool uses become --flags.
18
- parser.add_commands(
19
- _dispatch_funcs, name_mapping_policy=NameMappingPolicy.BY_NAME_IF_KWONLY
20
- )
21
- try:
22
- import argcomplete
23
-
24
- argcomplete.autocomplete(parser)
25
- except ImportError:
26
- pass
27
- parser.dispatch()
28
-
29
-
30
- if __name__ == "__main__":
31
- main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes