terp-cli 0.27.0__tar.gz → 0.28.0__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 (40) hide show
  1. {terp_cli-0.27.0 → terp_cli-0.28.0}/PKG-INFO +8 -8
  2. {terp_cli-0.27.0 → terp_cli-0.28.0}/pyproject.toml +8 -8
  3. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/__init__.py +25 -9
  4. terp_cli-0.28.0/src/terp/cli/_output.py +60 -0
  5. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/capabilities.py +19 -1
  6. terp_cli-0.28.0/src/terp/cli/dev.py +472 -0
  7. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/verify.py +117 -7
  8. terp_cli-0.27.0/src/terp/cli/_output.py +0 -26
  9. terp_cli-0.27.0/src/terp/cli/dev.py +0 -240
  10. {terp_cli-0.27.0 → terp_cli-0.28.0}/.gitignore +0 -0
  11. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/_appref.py +0 -0
  12. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/_engine.py +0 -0
  13. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/_subjects.py +0 -0
  14. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/access.py +0 -0
  15. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/apidocs.py +0 -0
  16. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/authz_surface.py +0 -0
  17. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/deploy_safety.py +0 -0
  18. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/docker.py +0 -0
  19. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/envfile.py +0 -0
  20. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/envschema.py +0 -0
  21. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/envseams.py +0 -0
  22. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/fmt.py +0 -0
  23. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/grants.py +0 -0
  24. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/jobs.py +0 -0
  25. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/leases.py +0 -0
  26. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/module_roles.py +0 -0
  27. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/openapi.py +0 -0
  28. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/outbox.py +0 -0
  29. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/ports.py +0 -0
  30. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/profiles.py +0 -0
  31. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/py.typed +0 -0
  32. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/routes.py +0 -0
  33. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/scaffold.py +0 -0
  34. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/schema.py +0 -0
  35. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/seed.py +0 -0
  36. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/service_accounts.py +0 -0
  37. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/smoke.py +0 -0
  38. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/users.py +0 -0
  39. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/version.py +0 -0
  40. {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/workbench.py +0 -0
@@ -1,19 +1,19 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: terp-cli
3
- Version: 0.27.0
3
+ Version: 0.28.0
4
4
  Summary: Terp command-line tool — inspect, scaffolding, migrations, checks, api-docs.
5
5
  Project-URL: Repository, https://github.com/AITT-NL/terp-framework
6
6
  Project-URL: Changelog, https://github.com/AITT-NL/terp-framework/blob/main/CHANGELOG.md
7
7
  License-Expression: Apache-2.0
8
8
  Requires-Python: >=3.13
9
9
  Requires-Dist: pyyaml>=6.0
10
- Requires-Dist: terp-arch==0.27.0
11
- Requires-Dist: terp-core==0.27.0
12
- Requires-Dist: terp-migrations==0.27.0
10
+ Requires-Dist: terp-arch==0.28.0
11
+ Requires-Dist: terp-core==0.28.0
12
+ Requires-Dist: terp-migrations==0.28.0
13
13
  Provides-Extra: jobs
14
- Requires-Dist: terp-cap-outbox==0.27.0; extra == 'jobs'
15
- Requires-Dist: terp-cap-scheduler-apscheduler==0.27.0; extra == 'jobs'
14
+ Requires-Dist: terp-cap-outbox==0.28.0; extra == 'jobs'
15
+ Requires-Dist: terp-cap-scheduler-apscheduler==0.28.0; extra == 'jobs'
16
16
  Provides-Extra: scheduler
17
- Requires-Dist: terp-cap-scheduler-apscheduler==0.27.0; extra == 'scheduler'
17
+ Requires-Dist: terp-cap-scheduler-apscheduler==0.28.0; extra == 'scheduler'
18
18
  Provides-Extra: worker
19
- Requires-Dist: terp-cap-outbox==0.27.0; extra == 'worker'
19
+ Requires-Dist: terp-cap-outbox==0.28.0; extra == 'worker'
@@ -4,18 +4,18 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "terp-cli"
7
- version = "0.27.0"
7
+ version = "0.28.0"
8
8
  description = "Terp command-line tool — inspect, scaffolding, migrations, checks, api-docs."
9
9
  requires-python = ">=3.13"
10
10
  license = "Apache-2.0"
11
11
  dependencies = [
12
- "terp-core==0.27.0",
12
+ "terp-core==0.28.0",
13
13
  # `terp migrate` delegates to terp-migrations (lazily imported), keeping Alembic
14
14
  # off the path for `terp inspect` / `terp guide` while making migrations work.
15
- "terp-migrations==0.27.0",
15
+ "terp-migrations==0.28.0",
16
16
  # `terp guide rules` projects the live rule registry; terp-arch is imported lazily
17
17
  # (only when that topic is rendered), so it stays off the common `terp guide` path.
18
- "terp-arch==0.27.0",
18
+ "terp-arch==0.28.0",
19
19
  # `terp verify --only env-seams` and `terp smoke` read the compose profiles as data
20
20
  # (anchors and `<<:` merge keys resolved), so both answer without a Docker daemon or
21
21
  # even the `docker` binary. Declared explicitly rather than leant on: every app
@@ -29,11 +29,11 @@ dependencies = [
29
29
  # or a scheduler engine into apps that only use inspect/check/migrate. Deployments can select
30
30
  # one process role, while `jobs` is the convenient complete jobs-process bundle.
31
31
  [project.optional-dependencies]
32
- worker = ["terp-cap-outbox==0.27.0"]
33
- scheduler = ["terp-cap-scheduler-apscheduler==0.27.0"]
32
+ worker = ["terp-cap-outbox==0.28.0"]
33
+ scheduler = ["terp-cap-scheduler-apscheduler==0.28.0"]
34
34
  jobs = [
35
- "terp-cap-outbox==0.27.0",
36
- "terp-cap-scheduler-apscheduler==0.27.0",
35
+ "terp-cap-outbox==0.28.0",
36
+ "terp-cap-scheduler-apscheduler==0.28.0",
37
37
  ]
38
38
 
39
39
  [project.scripts]
@@ -16,7 +16,7 @@ from terp.core import ControlPlane, CorsPolicy, ModuleSpec
16
16
  if TYPE_CHECKING: # terp-arch stays off the common `terp guide` / `terp inspect` path
17
17
  from terp.arch import ScanRoot
18
18
 
19
- from terp.cli._output import emit
19
+ from terp.cli._output import emit, use_utf8_output
20
20
  from terp.cli.access import (
21
21
  build_access_graph_for_app,
22
22
  render_access,
@@ -229,8 +229,11 @@ Adopt it by declaring the table (a project from the template already has it):
229
229
 
230
230
  [tool.deptry.per_rule_ignores]
231
231
  # `terp.*` is one PEP 420 namespace across distributions, and pydantic
232
- # re-exports through sqlmodel / pydantic-settings.
233
- DEP003 = ["terp", "pydantic"]
232
+ # re-exports through sqlmodel / pydantic-settings. fastapi, sqlmodel,
233
+ # sqlalchemy and alembic are the stack terp-core and terp-migrations bring,
234
+ # which the template's code, `terp new module` and `terp migrate make` all
235
+ # import directly: constrained by that release, not by a second pin here.
236
+ DEP003 = ["terp", "pydantic", "fastapi", "sqlmodel", "sqlalchemy", "alembic"]
234
237
 
235
238
  and installing the tool: `uv add --dev deptry`.
236
239
 
@@ -1759,6 +1762,10 @@ Frontend module screens (@terpjs/react-core)
1759
1762
  style={} / className / module stylesheets -> layout via Stack/DetailList; design tokens
1760
1763
  <a href="/..."> -> the router's Link (role-aware, no reload)
1761
1764
  deep imports (@terpjs/*/src, @terpjs/*/dist) -> import from the package root only
1765
+ data-terp / data-terp-* anywhere in src -> compose the component that renders it
1766
+ The data-terp markers are react-core's own: its stylesheet and the runtime layout check
1767
+ trust them. A framework screen you replace (renderTerpApp({ login })) is yours, so its
1768
+ tests find it by role and accessible name, not by the framework screen's markers.
1762
1769
  - Frontend security defaults (each its own lint rule, same error-only footing):
1763
1770
  dangerouslySetInnerHTML and DOM HTML-injection sinks (innerHTML/outerHTML/
1764
1771
  insertAdjacentHTML/document.write) are refused — render text, or Markdown from
@@ -1784,6 +1791,12 @@ Frontend module screens (@terpjs/react-core)
1784
1791
  OpenAPI export) and unwrap(...) which throws a typed ApiError carrying code/status.
1785
1792
  - The one governed opt-out is a justified `// terp-allow-<rule>: <reason>` marker whose
1786
1793
  counts must exactly match the app's checked-in escape-hatch-budget.json (a ratchet).
1794
+ - Unit tests sit beside the code as src/**/*.test.tsx and run with
1795
+ npm --prefix frontend test (vitest in jsdom; `terp verify` runs it too). jsdom has no
1796
+ <dialog> modal API, so frontend/vitest.setup.ts calls installDialogPolyfill() from
1797
+ @terpjs/react-core/testing: a test can open a ConfirmDialog and confirm it, cancel it or
1798
+ press Escape. Focus and the inert page behind a modal are not emulated there; they
1799
+ belong to the browser suite in conformance/.
1787
1800
  - Run the lint locally: npm --prefix frontend run lint (part of the gate).
1788
1801
  """,
1789
1802
  "dataview": """\
@@ -3578,19 +3591,21 @@ def _build_parser() -> argparse.ArgumentParser:
3578
3591
  dev_parser.add_argument(
3579
3592
  "--port",
3580
3593
  type=int,
3581
- default=DEFAULT_API_PORT,
3594
+ default=None,
3582
3595
  help=(
3583
- f"Backend host port (default: {DEFAULT_API_PORT}) -- in the range Terp owns, "
3584
- "away from the 8000 another application on this machine is probably using"
3596
+ "Backend host port (default: the pair claimed for this checkout, the one "
3597
+ "`terp ports show` prints and a workbench and docker compose use; claimed "
3598
+ f"and published on first run; {DEFAULT_API_PORT} if none can be claimed)"
3585
3599
  ),
3586
3600
  )
3587
3601
  dev_parser.add_argument(
3588
3602
  "--web-port",
3589
3603
  type=int,
3590
- default=DEFAULT_WEB_PORT,
3604
+ default=None,
3591
3605
  help=(
3592
- f"Frontend host port (default: {DEFAULT_WEB_PORT}); passed through to the "
3593
- "frontend dev server, which would otherwise take its own 5173"
3606
+ "Frontend host port (default: the web half of the same claimed pair; "
3607
+ f"{DEFAULT_WEB_PORT} if none can be claimed); the frontend dev server refuses "
3608
+ "a taken port rather than wandering to the next one"
3594
3609
  ),
3595
3610
  )
3596
3611
  dev_parser.add_argument(
@@ -3932,6 +3947,7 @@ def _build_parser() -> argparse.ArgumentParser:
3932
3947
 
3933
3948
  def main(argv: Sequence[str] | None = None) -> None:
3934
3949
  """Console entry point."""
3950
+ use_utf8_output()
3935
3951
  parser = _build_parser()
3936
3952
  args = parser.parse_args(argv)
3937
3953
  if args.command == "inspect" and args.inspect_command == "control-plane":
@@ -0,0 +1,60 @@
1
+ """The one place ``terp``'s command dispatch writes to standard output.
2
+
3
+ A CLI's job is to print, so ``no_print`` is not a defect report against this package --
4
+ it is the reason ``packages/backend/cli`` is recorded in
5
+ ``packages/backend/UNSCANNED.json`` rather than scanned. What the record is for is the
6
+ direction of travel: a count there may fall and never rise, so the dispatcher acquiring
7
+ one more bare ``print`` per command added is the shape that makes it rise forever.
8
+
9
+ One seam fixes that and is worth having on its own terms. Everything the dispatcher
10
+ says to an operator goes through here, which is where a ``--quiet``, a machine-readable
11
+ envelope, or a decision to route diagnostics to stderr would go. Thirty-eight call sites
12
+ answering that question independently is thirty-eight places such a change has to find.
13
+
14
+ Deliberately not a logger. This is a command's ANSWER, not a diagnostic about producing
15
+ it: it belongs on stdout unconditionally, it is what a shell pipes into the next thing,
16
+ and a log level that could suppress it would make the tool lie about having run.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import codecs
22
+ import sys
23
+
24
+ __all__ = ["emit", "use_utf8_output"]
25
+
26
+
27
+ def emit(text: object = "") -> None:
28
+ """Write one line of command output to stdout, flushed.
29
+
30
+ Flushed because a pipe is block-buffered: a one-shot command loses nothing by it, but
31
+ ``terp dev`` runs until it is stopped, and a line saying it restarted the backend is only
32
+ worth reading while that is true. Unflushed, it sat in the buffer until the process ended,
33
+ and a stopped process's buffer is simply lost.
34
+ """
35
+ print(text, flush=True)
36
+
37
+
38
+ def use_utf8_output() -> None:
39
+ """Write standard output and error as UTF-8 where the stream would not.
40
+
41
+ On Windows a pipe or a file takes the ANSI code page -- cp1252 on a Western install --
42
+ and a pipe is exactly how ``terp`` is read by an agent, an editor task or a workbench,
43
+ none of which is a console. cp1252 has no ``→``, so the line ``terp dev`` prints before
44
+ it starts the servers ended the command with ``UnicodeEncodeError`` instead; and text
45
+ this CLI does not author -- a module's label, a finding quoting a source line -- can do
46
+ the same to any command. UTF-8 encodes every string, and it is the default Python itself
47
+ moves to for these streams (PEP 686).
48
+
49
+ Changed only where it is not already UTF-8, so a console (which Python already drives
50
+ through the console API as UTF-8) and every Linux or macOS terminal are left exactly as
51
+ they were. A stream that cannot be reconfigured -- none at all, or a replacement such
52
+ as ``io.StringIO`` -- is left alone. ``backslashreplace`` rather than ``strict``, so the
53
+ one thing UTF-8 cannot encode, a lone surrogate from an undecodable file name, prints
54
+ as an escape rather than ending the command.
55
+ """
56
+ for stream in (sys.stdout, sys.stderr):
57
+ reconfigure = getattr(stream, "reconfigure", None)
58
+ if reconfigure is None or codecs.lookup(stream.encoding).name == "utf-8":
59
+ continue
60
+ reconfigure(encoding="utf-8", errors="backslashreplace")
@@ -44,6 +44,17 @@ class Capability:
44
44
  guide: str | None = None
45
45
  """``terp guide`` topic covering it, when one exists."""
46
46
 
47
+ inventory: str | None = None
48
+ """The command that lists what this app holds of it, when the CLI has one.
49
+
50
+ The registry is built from packages, so it can say an app *has* a capability and
51
+ nothing about what the app has *done* with it. That gap misled a reader into
52
+ concluding an app had no machine credentials because the identity line said only
53
+ "user store": the service accounts it had issued were one command away, and nothing
54
+ on this screen named the command. Rendered for installed capabilities only — an
55
+ unadopted one has nothing to list.
56
+ """
57
+
47
58
  @property
48
59
  def distribution(self) -> str:
49
60
  """The PyPI distribution name."""
@@ -62,6 +73,7 @@ CAPABILITIES: tuple[Capability, ...] = (
62
73
  kind="routed",
63
74
  wiring="create_app(..., discover_capabilities=True)",
64
75
  guide="access",
76
+ inventory="terp grant list <subject>",
65
77
  ),
66
78
  Capability(
67
79
  name="audit",
@@ -106,9 +118,11 @@ CAPABILITIES: tuple[Capability, ...] = (
106
118
  ),
107
119
  Capability(
108
120
  name="identity",
109
- summary="Persisted user store backing authentication.",
121
+ summary="Persisted user store backing authentication, and the service accounts an integration signs in with.",
110
122
  kind="library",
111
123
  wiring="create_app(..., discover_capabilities=True)",
124
+ guide="package-boundaries",
125
+ inventory="terp service-account list",
112
126
  ),
113
127
  Capability(
114
128
  name="jobs_celery",
@@ -130,6 +144,7 @@ CAPABILITIES: tuple[Capability, ...] = (
130
144
  kind="library",
131
145
  wiring="create_app(..., lease_store=DatabaseLeaseStore()) + specs=[leases.module]",
132
146
  guide="leases",
147
+ inventory="terp leases list",
133
148
  ),
134
149
  Capability(
135
150
  name="mail",
@@ -385,6 +400,7 @@ def render_capabilities(*, fmt: str = "text", root: str | pathlib.Path = ".") ->
385
400
  "kind": cap.kind,
386
401
  "wiring": cap.wiring,
387
402
  "guide": cap.guide,
403
+ "inventory": cap.inventory,
388
404
  "installed": version is not None,
389
405
  "version": version,
390
406
  "seams": list(seams.get(cap.name, ())),
@@ -424,6 +440,8 @@ def render_capabilities(*, fmt: str = "text", root: str | pathlib.Path = ".") ->
424
440
  for cap, version in installed:
425
441
  lines.append(f" {cap.distribution:<32} {version:<10} {cap.kind}")
426
442
  lines.append(f" {cap.summary}")
443
+ if cap.inventory is not None:
444
+ lines.append(f" what this app has: uv run {cap.inventory}")
427
445
  missing = unwired.get(cap.name, ())
428
446
  if missing:
429
447
  lines.append(f" not used here: {', '.join(missing)}")