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.
- {terp_cli-0.27.0 → terp_cli-0.28.0}/PKG-INFO +8 -8
- {terp_cli-0.27.0 → terp_cli-0.28.0}/pyproject.toml +8 -8
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/__init__.py +25 -9
- terp_cli-0.28.0/src/terp/cli/_output.py +60 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/capabilities.py +19 -1
- terp_cli-0.28.0/src/terp/cli/dev.py +472 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/verify.py +117 -7
- terp_cli-0.27.0/src/terp/cli/_output.py +0 -26
- terp_cli-0.27.0/src/terp/cli/dev.py +0 -240
- {terp_cli-0.27.0 → terp_cli-0.28.0}/.gitignore +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/_appref.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/_engine.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/_subjects.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/access.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/apidocs.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/authz_surface.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/deploy_safety.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/docker.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/envfile.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/envschema.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/envseams.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/fmt.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/grants.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/jobs.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/leases.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/module_roles.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/openapi.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/outbox.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/ports.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/profiles.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/py.typed +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/routes.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/scaffold.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/schema.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/seed.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/service_accounts.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/smoke.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/users.py +0 -0
- {terp_cli-0.27.0 → terp_cli-0.28.0}/src/terp/cli/version.py +0 -0
- {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.
|
|
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.
|
|
11
|
-
Requires-Dist: terp-core==0.
|
|
12
|
-
Requires-Dist: terp-migrations==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.
|
|
15
|
-
Requires-Dist: terp-cap-scheduler-apscheduler==0.
|
|
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.
|
|
17
|
+
Requires-Dist: terp-cap-scheduler-apscheduler==0.28.0; extra == 'scheduler'
|
|
18
18
|
Provides-Extra: worker
|
|
19
|
-
Requires-Dist: terp-cap-outbox==0.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
33
|
-
scheduler = ["terp-cap-scheduler-apscheduler==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.
|
|
36
|
-
"terp-cap-scheduler-apscheduler==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
|
-
|
|
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=
|
|
3594
|
+
default=None,
|
|
3582
3595
|
help=(
|
|
3583
|
-
|
|
3584
|
-
"
|
|
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=
|
|
3604
|
+
default=None,
|
|
3591
3605
|
help=(
|
|
3592
|
-
|
|
3593
|
-
"
|
|
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)}")
|