prodc 0.3.7__tar.gz → 0.3.8__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.
- {prodc-0.3.7 → prodc-0.3.8}/PKG-INFO +1 -1
- {prodc-0.3.7 → prodc-0.3.8}/pyproject.toml +1 -1
- {prodc-0.3.7 → prodc-0.3.8}/src/prodc/cli.py +52 -0
- {prodc-0.3.7 → prodc-0.3.8}/tests/test_cli.py +24 -0
- {prodc-0.3.7 → prodc-0.3.8}/.gitignore +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/CHANGELOG.md +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/LICENSE +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/README.md +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/examples/quickstart/features/orders.feature +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/examples/quickstart/needs/jordan.yaml +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/examples/quickstart/prodc.toml +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/src/prodc/__init__.py +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/src/prodc/adapters.py +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/src/prodc/config.py +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/src/prodc/loader.py +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/src/prodc/model.py +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/src/prodc/py.typed +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/src/prodc/status.py +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/tests/test_adapters.py +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/tests/test_loader.py +0 -0
- {prodc-0.3.7 → prodc-0.3.8}/tests/test_status.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: prodc
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: Product management as code: personas, flows, and features traced to evidence.
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/jorgeecardona/prodc
|
|
6
6
|
Project-URL: Repository, https://gitlab.com/jorgeecardona/prodc
|
|
@@ -13,6 +13,7 @@ import shutil
|
|
|
13
13
|
import sys
|
|
14
14
|
from dataclasses import asdict, dataclass
|
|
15
15
|
from pathlib import Path
|
|
16
|
+
from typing import cast
|
|
16
17
|
|
|
17
18
|
from . import __version__
|
|
18
19
|
from .config import Config, ConfigError
|
|
@@ -380,6 +381,51 @@ def _cmd_portfolio(args: argparse.Namespace) -> int:
|
|
|
380
381
|
return 2 if any(r.error is not None for r in rows) else 0
|
|
381
382
|
|
|
382
383
|
|
|
384
|
+
def _cmd_diff(args: argparse.Namespace) -> int:
|
|
385
|
+
"""Diff the current board against a prior `status --json` snapshot."""
|
|
386
|
+
loaded = _load_or_report(args)
|
|
387
|
+
if loaded is None:
|
|
388
|
+
return 2
|
|
389
|
+
cfg, needs = loaded
|
|
390
|
+
current = {r.need.id: r.state.value for r in resolve(needs, cfg.adapters, cfg.source_adapters)}
|
|
391
|
+
|
|
392
|
+
snap = Path(args.snapshot)
|
|
393
|
+
try:
|
|
394
|
+
payload: object = json.loads(snap.read_text(encoding="utf-8"))
|
|
395
|
+
except (OSError, ValueError) as exc:
|
|
396
|
+
print(f"prodc: cannot read snapshot {snap}: {exc}", file=sys.stderr)
|
|
397
|
+
return 2
|
|
398
|
+
raw_needs = cast("dict[str, object]", payload).get("needs") if isinstance(payload, dict) else None
|
|
399
|
+
old: dict[str, str] = {}
|
|
400
|
+
for n in cast("list[object]", raw_needs) if isinstance(raw_needs, list) else []:
|
|
401
|
+
if isinstance(n, dict):
|
|
402
|
+
entry = cast("dict[str, object]", n)
|
|
403
|
+
nid, st = entry.get("id"), entry.get("state")
|
|
404
|
+
if isinstance(nid, str) and isinstance(st, str):
|
|
405
|
+
old[nid] = st
|
|
406
|
+
|
|
407
|
+
new = {i: current[i] for i in sorted(current.keys() - old.keys())}
|
|
408
|
+
removed = sorted(old.keys() - current.keys())
|
|
409
|
+
changed = {
|
|
410
|
+
i: [old[i], current[i]] for i in sorted(old.keys() & current.keys()) if old[i] != current[i]
|
|
411
|
+
}
|
|
412
|
+
if args.json:
|
|
413
|
+
print(json.dumps({"new": new, "removed": removed, "changed": changed}, indent=2))
|
|
414
|
+
return 0
|
|
415
|
+
if not (new or removed or changed):
|
|
416
|
+
print("no changes since snapshot")
|
|
417
|
+
return 0
|
|
418
|
+
out: list[str] = []
|
|
419
|
+
for i, st in new.items():
|
|
420
|
+
out.append(f" new {i:<10} ({st})")
|
|
421
|
+
for i in removed:
|
|
422
|
+
out.append(f" removed {i}")
|
|
423
|
+
for i, (was, now) in changed.items():
|
|
424
|
+
out.append(f" changed {i:<10} {was} → {now}")
|
|
425
|
+
print("\n".join(out))
|
|
426
|
+
return 0
|
|
427
|
+
|
|
428
|
+
|
|
383
429
|
def _git_rev(root: Path) -> str | None:
|
|
384
430
|
head = root / ".git" / "HEAD"
|
|
385
431
|
try:
|
|
@@ -437,6 +483,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
437
483
|
pf.add_argument("--json", action="store_true", help="machine-readable output")
|
|
438
484
|
pf.set_defaults(func=_cmd_portfolio)
|
|
439
485
|
|
|
486
|
+
df = sub.add_parser("diff", help="diff the current board against a prior status --json snapshot")
|
|
487
|
+
df.add_argument("snapshot", help="path to a saved `prodc status --json` file")
|
|
488
|
+
df.add_argument("--config", default="prodc.toml", help="path to prodc.toml")
|
|
489
|
+
df.add_argument("--json", action="store_true", help="machine-readable output")
|
|
490
|
+
df.set_defaults(func=_cmd_diff)
|
|
491
|
+
|
|
440
492
|
args = parser.parse_args(argv)
|
|
441
493
|
return int(args.func(args))
|
|
442
494
|
|
|
@@ -179,6 +179,30 @@ def test_portfolio_bad_config_exits_2(tmp_path: Path, capsys: pytest.CaptureFixt
|
|
|
179
179
|
assert code == 2
|
|
180
180
|
|
|
181
181
|
|
|
182
|
+
def test_diff_no_changes(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
|
183
|
+
toml = str(_project(tmp_path))
|
|
184
|
+
main(["status", "--config", toml, "--json"])
|
|
185
|
+
snap = tmp_path / "snap.json"
|
|
186
|
+
snap.write_text(capsys.readouterr().out, encoding="utf-8")
|
|
187
|
+
main(["diff", str(snap), "--config", toml])
|
|
188
|
+
assert "no changes" in capsys.readouterr().out
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_diff_reports_new_need(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
|
192
|
+
toml = str(_project(tmp_path))
|
|
193
|
+
main(["status", "--config", toml, "--json"])
|
|
194
|
+
snap = tmp_path / "snap.json"
|
|
195
|
+
snap.write_text(capsys.readouterr().out, encoding="utf-8")
|
|
196
|
+
# add a brand-new need, then diff against the snapshot
|
|
197
|
+
(tmp_path / "needs" / "a.yaml").write_text(
|
|
198
|
+
_NEEDS + " - id: X-9\n text: a newly added need\n", encoding="utf-8"
|
|
199
|
+
)
|
|
200
|
+
code = main(["diff", str(snap), "--config", toml])
|
|
201
|
+
out = capsys.readouterr().out
|
|
202
|
+
assert code == 0
|
|
203
|
+
assert "new" in out and "X-9" in out
|
|
204
|
+
|
|
205
|
+
|
|
182
206
|
def test_proxy_source_kind_warrant(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
|
183
207
|
import json
|
|
184
208
|
|
|
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
|