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.
- {paces-0.0.6 → paces-0.0.7}/PKG-INFO +3 -3
- paces-0.0.7/paces/__main__.py +39 -0
- {paces-0.0.6 → paces-0.0.7}/paces/tools.py +1 -1
- {paces-0.0.6 → paces-0.0.7}/pyproject.toml +4 -4
- paces-0.0.7/tests/test_cli_surface.py +133 -0
- paces-0.0.6/paces/__main__.py +0 -31
- {paces-0.0.6 → paces-0.0.7}/.gitignore +0 -0
- {paces-0.0.6 → paces-0.0.7}/LICENSE +0 -0
- {paces-0.0.6 → paces-0.0.7}/README.md +0 -0
- {paces-0.0.6 → paces-0.0.7}/docs/README.md +0 -0
- {paces-0.0.6 → paces-0.0.7}/docs/alignment/README.md +0 -0
- {paces-0.0.6 → paces-0.0.7}/docs/poc-reference/README.md +0 -0
- {paces-0.0.6 → paces-0.0.7}/docs/poc-reference/artifacts/clips.json +0 -0
- {paces-0.0.6 → paces-0.0.7}/paces/__init__.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/paces/derivation.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/paces/edits.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/paces/excerpts.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/paces/measure.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/paces/model.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/paces/projection.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/paces/render.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/paces/segmenters.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/audio_synth.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/data/routine.json +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/poc_fixture.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/test_chapters.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/test_derivation.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/test_edits.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/test_excerpts.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/test_measure.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/test_render_media.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/test_roundtrip_poc.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/test_segment.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/test_smoke.py +0 -0
- {paces-0.0.6 → paces-0.0.7}/tests/test_vertical_slice.py +0 -0
- {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.
|
|
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:
|
|
20
|
+
Requires-Dist: cw<0.2,>=0.1.1; extra == 'cli'
|
|
21
21
|
Provides-Extra: dev
|
|
22
|
-
Requires-Dist:
|
|
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 ``
|
|
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.
|
|
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 = ["
|
|
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
|
|
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
|
-
"
|
|
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 == ""
|
paces-0.0.6/paces/__main__.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|