terp-cli 0.32.0__tar.gz → 0.34.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.32.0 → terp_cli-0.34.0}/PKG-INFO +8 -8
- {terp_cli-0.32.0 → terp_cli-0.34.0}/pyproject.toml +8 -8
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/__init__.py +69 -10
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/verify.py +57 -62
- {terp_cli-0.32.0 → terp_cli-0.34.0}/.gitignore +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/_appref.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/_engine.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/_output.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/_subjects.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/access.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/apidocs.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/authz_surface.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/capabilities.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/deploy_safety.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/dev.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/docker.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/docker_masks.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/envfile.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/envschema.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/envseams.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/fmt.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/grants.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/jobs.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/leases.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/module_roles.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/openapi.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/outbox.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/ports.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/profiles.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/py.typed +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/routes.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/scaffold.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/schema.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/seed.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/service_accounts.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/smoke.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/users.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.0}/src/terp/cli/version.py +0 -0
- {terp_cli-0.32.0 → terp_cli-0.34.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.34.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.34.0
|
|
11
|
+
Requires-Dist: terp-core==0.34.0
|
|
12
|
+
Requires-Dist: terp-migrations==0.34.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.34.0; extra == 'jobs'
|
|
15
|
+
Requires-Dist: terp-cap-scheduler-apscheduler==0.34.0; extra == 'jobs'
|
|
16
16
|
Provides-Extra: scheduler
|
|
17
|
-
Requires-Dist: terp-cap-scheduler-apscheduler==0.
|
|
17
|
+
Requires-Dist: terp-cap-scheduler-apscheduler==0.34.0; extra == 'scheduler'
|
|
18
18
|
Provides-Extra: worker
|
|
19
|
-
Requires-Dist: terp-cap-outbox==0.
|
|
19
|
+
Requires-Dist: terp-cap-outbox==0.34.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.34.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.34.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.34.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.34.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.34.0"]
|
|
33
|
+
scheduler = ["terp-cap-scheduler-apscheduler==0.34.0"]
|
|
34
34
|
jobs = [
|
|
35
|
-
"terp-cap-outbox==0.
|
|
36
|
-
"terp-cap-scheduler-apscheduler==0.
|
|
35
|
+
"terp-cap-outbox==0.34.0",
|
|
36
|
+
"terp-cap-scheduler-apscheduler==0.34.0",
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
[project.scripts]
|
|
@@ -683,6 +683,11 @@ Stored references (Ref + OnDelete)
|
|
|
683
683
|
chosen), and a database reports its default action as absent rather than as the
|
|
684
684
|
words NO ACTION -- which is why emitting the literal would make every
|
|
685
685
|
model-versus-database comparison report drift on that constraint forever.
|
|
686
|
+
- A reference you WRITE must point at a row you could READ. The foreign key checks that
|
|
687
|
+
the target exists; every flush also checks it against the target's row scope
|
|
688
|
+
(tenancy, soft-delete, an owner read scope), on insert and whenever an update changes
|
|
689
|
+
the reference - however the flush is reached, an autoflush included. Out of scope
|
|
690
|
+
reads exactly like missing: the same 409.
|
|
686
691
|
- Ref() forwards everything else to Field(), and indexes by default: an unindexed
|
|
687
692
|
foreign key turns every parent delete and every join into a table scan.
|
|
688
693
|
owner_id: uuid.UUID | None = Ref("user.id", on_delete=OnDelete.SET_NULL,
|
|
@@ -1007,6 +1012,11 @@ Multi-tenant rows (tenancy capability)
|
|
|
1007
1012
|
class Doc(BaseTable, TenantScopedMixin, table=True): ...
|
|
1008
1013
|
class DocService(TenantScopedService[Doc, DocCreate, DocUpdate]): model = Doc
|
|
1009
1014
|
- Never filter tenant_id by hand — the framework owns the predicate (the gate forbids it).
|
|
1015
|
+
- A reference stays inside its tenant. Every flush checks each Ref it writes against
|
|
1016
|
+
the target model's row scope, so a row cannot point at another
|
|
1017
|
+
tenant's row (nor at a soft-deleted one): the write fails with the same 409 a missing
|
|
1018
|
+
target gets, and says nothing about the other tenant. Nothing to wire - it follows from
|
|
1019
|
+
the predicate the mixin registers.
|
|
1010
1020
|
- The current tenant comes from the request (TenantMiddleware binds the JWT `tenant`
|
|
1011
1021
|
claim); in tests use tenant_context(tenant_id).
|
|
1012
1022
|
- Wire it through the create_app middleware seam — never add_middleware (the gate
|
|
@@ -1454,8 +1464,23 @@ Realtime push (realtime capability)
|
|
|
1454
1464
|
- Wire the runtime seams once at the composition root:
|
|
1455
1465
|
configure_realtime(permission_enforcer=..., principal_validator=...,
|
|
1456
1466
|
message_session_provider=...)
|
|
1457
|
-
|
|
1458
|
-
|
|
1467
|
+
- The default broker and ticket store are PER PROCESS: a publish reaches only subscribers
|
|
1468
|
+
connected to the process that published. That breaks with a second replica, and it
|
|
1469
|
+
breaks with ONE replica too once a job handler runs in `terp jobs worker` - the worker
|
|
1470
|
+
serves no transport, so its publish reaches nobody. Share both through Redis
|
|
1471
|
+
(terp-cap-redis[realtime]) and promise it, so a missing wiring fails instead of
|
|
1472
|
+
dropping messages:
|
|
1473
|
+
configure_broker(RedisRealtimeBroker.from_url(settings.REDIS_URL))
|
|
1474
|
+
configure_ticket_store(RedisConnectionTicketStore.from_url(settings.REDIS_URL))
|
|
1475
|
+
configure_realtime(..., require_shared_broker=settings.is_production)
|
|
1476
|
+
The worker runs the same composition root, so it wires the same broker. Pub/sub is
|
|
1477
|
+
server-wide: the database index is in the channel names, but two deployments sharing
|
|
1478
|
+
one Redis server AND database need distinct namespace=... values. Delivery is
|
|
1479
|
+
fire-and-forget either way: a message published while nobody listens is gone, and a
|
|
1480
|
+
publish Redis does not take (connecting and the reply are each bounded at 2s) is
|
|
1481
|
+
dropped with a logged warning - it never fails the write that published it. A
|
|
1482
|
+
transport subscribes before it answers, so an unreachable Redis is an SSE 503 or a
|
|
1483
|
+
WebSocket closed 1013 before accept, and the browser backs off.
|
|
1459
1484
|
- Transport is TICKET-based, never a token in a URL: the client POSTs
|
|
1460
1485
|
/api/v1/realtime/tickets, receives a one-use short-lived ticket, then connects to
|
|
1461
1486
|
/api/v1/realtime/sse/<channel>?ticket=... (or /ws/<channel> in websocket mode). The
|
|
@@ -1579,6 +1604,26 @@ Using capabilities
|
|
|
1579
1604
|
and egress auditing attach. A sanctioned internal target is a declared
|
|
1580
1605
|
`allow_private_addresses=True`, visible in the composition root, never a quiet
|
|
1581
1606
|
exception inside the client.
|
|
1607
|
+
- A vendor SDK builds its own HTTP client, so neither the rule (it reads YOUR imports)
|
|
1608
|
+
nor EgressClient sees its traffic. Hold the whole process to the declaration at the
|
|
1609
|
+
socket, in the composition root the web process and the worker both run:
|
|
1610
|
+
install_egress_guard(EgressGuard.for_policies(
|
|
1611
|
+
rates_policy, # every EgressPolicy you declared
|
|
1612
|
+
hosts=("api.vendor.example",), # what an SDK calls, by exact name
|
|
1613
|
+
infrastructure=("db", "redis", "smtp.relay.internal"), # names, IPs or CIDRs
|
|
1614
|
+
))
|
|
1615
|
+
It holds what goes through Python's socket module on IPv4/IPv6, whichever library
|
|
1616
|
+
calls it: a lookup of an undeclared name (getaddrinfo, gethostbyname) is refused
|
|
1617
|
+
before it is made, and a connect or datagram into a private / loopback / link-local /
|
|
1618
|
+
metadata range is refused unless it is declared infrastructure; a peer given by name
|
|
1619
|
+
(localhost too) is resolved and every address held. The refusal is EgressRefusedError
|
|
1620
|
+
(502); under an async client it can arrive inside an ExceptionGroup. It cannot see
|
|
1621
|
+
uvloop, so it refuses to install beside it and refuses `import uvloop` after it: run
|
|
1622
|
+
uvicorn with --loop asyncio (uvicorn[standard] picks uvloop otherwise), and a worker
|
|
1623
|
+
or scheduler with asyncio.run. Native drivers that open their own sockets (libpq,
|
|
1624
|
+
gRPC's C core) are invisible to it and a public IP literal passes; the deployment's
|
|
1625
|
+
network policy holds those. Built for Linux: on Windows asyncio's socketpair connects
|
|
1626
|
+
to loopback, so a new event loop is refused unless loopback is declared.
|
|
1582
1627
|
- Credentials never live in module source: a credential-shaped assignment (password,
|
|
1583
1628
|
api_key, token, ...) to a string literal — or a recognizable secret-token literal
|
|
1584
1629
|
anywhere — is refused by the no_hardcoded_credentials rule. Wire secrets through
|
|
@@ -1778,28 +1823,36 @@ Frontend module screens (@terpjs/react-core)
|
|
|
1778
1823
|
everything composes the token-styled @terpjs/react-core surface. The full catalog (with
|
|
1779
1824
|
per-export "Use" guidance) is the @terpjs/react-core README; each export also carries
|
|
1780
1825
|
JSDoc, so your editor shows the same guidance inline.
|
|
1781
|
-
- The boundary lint (@terpjs/eslint-boundaries) refuses, fail-closed
|
|
1826
|
+
- The boundary lint (@terpjs/eslint-boundaries) refuses, fail-closed, in every file under
|
|
1827
|
+
frontend/src/ — a component beside the modules is held exactly like one inside them:
|
|
1782
1828
|
raw <button>/<input>/<select>/<textarea> -> Button / Input / Select / Textarea
|
|
1783
1829
|
raw <table> -> DataView (terp guide dataview)
|
|
1784
1830
|
raw <dialog> -> ConfirmDialog
|
|
1785
1831
|
raw <form> -> Stack as="form" (terp guide forms)
|
|
1786
1832
|
raw fetch / XMLHttpRequest -> useTerpClient() + unwrap (typed client)
|
|
1787
1833
|
WebSocket / EventSource / sendBeacon -> the generated client (one egress path)
|
|
1788
|
-
style={} / className /
|
|
1834
|
+
style={} / className / any stylesheet -> layout via Stack/DetailList; design tokens
|
|
1789
1835
|
<a href="/..."> -> the router's Link (role-aware, no reload)
|
|
1790
1836
|
deep imports (@terpjs/*/src, @terpjs/*/dist) -> import from the package root only
|
|
1791
1837
|
data-terp / data-terp-* anywhere in src -> compose the component that renders it
|
|
1792
1838
|
The data-terp markers are react-core's own: its stylesheet and the runtime layout check
|
|
1793
1839
|
trust them. A framework screen you replace (renderTerpApp({ login })) is yours, so its
|
|
1794
1840
|
tests find it by role and accessible name, not by the framework screen's markers.
|
|
1841
|
+
The bootstrap (src/main.tsx) is the one file that imports stylesheets, and only the token
|
|
1842
|
+
pipeline's three: @terpjs/contract/tokens.css, ./house-style.css and ./theme.css. A
|
|
1843
|
+
library's stylesheet is refused there too: it would paint outside the palettes. A
|
|
1844
|
+
stylesheet loaded through import() or import.meta.glob is refused the same way.
|
|
1845
|
+
Modules stay independent: a module never imports a sibling, and code outside the modules
|
|
1846
|
+
(a shared helper) never imports from one, since shared code is what modules depend on.
|
|
1847
|
+
The bootstrap finds the modules with import.meta.glob, which is not an import.
|
|
1795
1848
|
- Frontend security defaults (each its own lint rule, same error-only footing):
|
|
1796
1849
|
dangerouslySetInnerHTML and DOM HTML-injection sinks (innerHTML/outerHTML/
|
|
1797
1850
|
insertAdjacentHTML/document.write) are refused — render text, or Markdown from
|
|
1798
1851
|
@terpjs/react-core for rich text; eval() / new Function() are refused; javascript:
|
|
1799
1852
|
URLs in href/src are refused; a static target="_blank" link needs rel="noopener".
|
|
1800
1853
|
- Every routed view renders a page archetype (Page / OverviewPage / DetailPage / HubPage /
|
|
1801
|
-
DashboardPage / FormPage / SettingsPage / SplitPage); buildAppRouter
|
|
1802
|
-
view at runtime, fail closed. An app can ratchet
|
|
1854
|
+
DashboardPage / FormPage / SettingsPage / SplitPage / WorkspacePage); buildAppRouter
|
|
1855
|
+
refuses an unframed view at runtime, fail closed. An app can ratchet
|
|
1803
1856
|
further with an opt-in slot-typed layout contract (terp guide layouts).
|
|
1804
1857
|
- Route paths and params are CHECKED, from generated types (ADR 0092). The router is built
|
|
1805
1858
|
at runtime from the manifests, so nothing type-checks a path or a param name until you
|
|
@@ -1873,7 +1926,7 @@ Forms (react-core primitives)
|
|
|
1873
1926
|
<Button type="submit" variant="primary">Save</Button>
|
|
1874
1927
|
</Stack>
|
|
1875
1928
|
- Field wraps label + control + hint/error for one field; Stack (vertical by default)
|
|
1876
|
-
is the layout — never style={} / className / a
|
|
1929
|
+
is the layout — never style={} / className / a stylesheet.
|
|
1877
1930
|
- Submit through the typed client: const client = useTerpClient();
|
|
1878
1931
|
await unwrap(client.POST("/api/v1/invoices/", { body })); a failure throws ApiError
|
|
1879
1932
|
({code, status, requestId, fields}) — map codes to copy with useErrorMessage, show
|
|
@@ -1938,8 +1991,8 @@ Theming and branding (design tokens, palettes, the brand mark)
|
|
|
1938
1991
|
|
|
1939
1992
|
- EVERY style is a design token. The react-core primitives paint from CSS custom
|
|
1940
1993
|
properties shipped by @terpjs/contract (tokens.css), which is why the boundary lint
|
|
1941
|
-
refuses `style={}`, `className` and
|
|
1942
|
-
would not follow the palette. Modules never need theme-specific code.
|
|
1994
|
+
refuses `style={}`, `className` and stylesheets in every src/ file: code that painted
|
|
1995
|
+
itself would not follow the palette. Modules never need theme-specific code.
|
|
1943
1996
|
- THE SHIPPED PALETTES, plus "system":
|
|
1944
1997
|
midday twilight evening night contrast
|
|
1945
1998
|
Named for the time of day they suit: midday is the light set, twilight a dimmed dark,
|
|
@@ -2095,6 +2148,10 @@ Layout contracts (slot-typed layouts, ADR 0079)
|
|
|
2095
2148
|
BarChart / ProportionBar / StatusHistory / Timeline / Divider / Text +
|
|
2096
2149
|
the same states and ConfirmDialog: "how is the whole doing", its
|
|
2097
2150
|
figures in the summary band
|
|
2151
|
+
WorkspacePage -> ONE CanvasHost + the same states and ConfirmDialog: one canvas that
|
|
2152
|
+
fills the screen. The host takes ONE canvas child (an svg, or a
|
|
2153
|
+
canvas library's root) and sizes it to the whole box; what is
|
|
2154
|
+
drawn ON it is yours. The trail is `parents`, as on DashboardPage
|
|
2098
2155
|
Grid is not an overview-body component, deliberately: an overview body is a data
|
|
2099
2156
|
collection, a grid of cards into each area is a hub, and sections of figures and charts
|
|
2100
2157
|
are a dashboard — each has its own archetype. Heading is admitted nowhere: a heading in a governed body must OWN its
|
|
@@ -2137,7 +2194,9 @@ Layout contracts (slot-typed layouts, ADR 0079)
|
|
|
2137
2194
|
app's areas is a HubPage; one area's collection (how each one is doing) an OverviewPage;
|
|
2138
2195
|
how the whole is doing a DashboardPage -- a landing that also carries the app's figures is
|
|
2139
2196
|
one, as the hub preset scaffolds it; one record a DetailPage; entering a record a
|
|
2140
|
-
FormPage; settings a SettingsPage; a list beside the record it selects a SplitPage
|
|
2197
|
+
FormPage; settings a SettingsPage; a list beside the record it selects a SplitPage; work
|
|
2198
|
+
done on a surface (a diagram of nodes and connections, a plan) a WorkspacePage, whose one
|
|
2199
|
+
CanvasHost takes every pixel the shell leaves - a canvas never needs a style to be sized.
|
|
2141
2200
|
- Alternate framed and unframed blocks. The frame belongs to the data a reader works with:
|
|
2142
2201
|
a collection, a chart and a lone figure sit on the surface, the collection brightest. A
|
|
2143
2202
|
boxed Card steps back onto --color-bg-subtle, for a group of controls or one section of
|
|
@@ -1090,6 +1090,45 @@ def _node_modules_problem(root: pathlib.Path, workspace: str) -> str | None:
|
|
|
1090
1090
|
)
|
|
1091
1091
|
|
|
1092
1092
|
|
|
1093
|
+
#: The status a shell gives a command it cannot find, and the one a check answers with
|
|
1094
|
+
#: when the tool it runs is not installed.
|
|
1095
|
+
_NOT_FOUND = 127
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def _run_argv(
|
|
1099
|
+
argv: list[str], root: pathlib.Path, *, env: dict[str, str] | None = None
|
|
1100
|
+
) -> subprocess.CompletedProcess[str]:
|
|
1101
|
+
"""Run *argv* in *root* as a fixed argv, with no shell; a missing tool is a result.
|
|
1102
|
+
|
|
1103
|
+
Every check that shells out comes through here, so a tool that is not installed
|
|
1104
|
+
answers the way a shell would, exit 127 naming the tool, instead of raising. Only the
|
|
1105
|
+
manifest runner used to catch it. The runners with a body of their own (route types,
|
|
1106
|
+
the API client, the API reference) let ``FileNotFoundError`` escape the profile loop:
|
|
1107
|
+
the run ended in a traceback about ``subprocess``, every check after that one went
|
|
1108
|
+
unrun and unreported, and the one fact worth reading, which tool is missing, was a
|
|
1109
|
+
frame deep in the stack.
|
|
1110
|
+
|
|
1111
|
+
The message is the result's stdout, so each caller's own formatting of a finished
|
|
1112
|
+
command carries it unchanged.
|
|
1113
|
+
"""
|
|
1114
|
+
executable = shutil.which(argv[0]) or argv[0]
|
|
1115
|
+
try:
|
|
1116
|
+
return subprocess.run( # noqa: S603 - fixed argv, shell=False
|
|
1117
|
+
[executable, *argv[1:]],
|
|
1118
|
+
cwd=root,
|
|
1119
|
+
capture_output=True,
|
|
1120
|
+
text=True,
|
|
1121
|
+
encoding="utf-8",
|
|
1122
|
+
errors="replace",
|
|
1123
|
+
check=False,
|
|
1124
|
+
env=None if env is None else {**os.environ, **env},
|
|
1125
|
+
)
|
|
1126
|
+
except FileNotFoundError:
|
|
1127
|
+
return subprocess.CompletedProcess(
|
|
1128
|
+
argv, _NOT_FOUND, stdout=f"{argv[0]}: executable not found on PATH", stderr=""
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
|
|
1093
1132
|
def _run_subprocess(
|
|
1094
1133
|
check: VerifyCheck, root: pathlib.Path, *, env: dict[str, str] | None = None
|
|
1095
1134
|
) -> tuple[int, str]:
|
|
@@ -1109,20 +1148,7 @@ def _run_subprocess(
|
|
|
1109
1148
|
problem = _node_modules_problem(root, _npm_workspace(argv))
|
|
1110
1149
|
if problem is not None:
|
|
1111
1150
|
return 1, problem
|
|
1112
|
-
|
|
1113
|
-
try:
|
|
1114
|
-
completed = subprocess.run( # noqa: S603 - fixed manifest argv, shell=False
|
|
1115
|
-
[executable, *argv[1:]],
|
|
1116
|
-
cwd=root,
|
|
1117
|
-
capture_output=True,
|
|
1118
|
-
text=True,
|
|
1119
|
-
encoding="utf-8",
|
|
1120
|
-
errors="replace",
|
|
1121
|
-
check=False,
|
|
1122
|
-
env=None if env is None else {**os.environ, **env},
|
|
1123
|
-
)
|
|
1124
|
-
except FileNotFoundError:
|
|
1125
|
-
return 127, f"{argv[0]}: executable not found on PATH"
|
|
1151
|
+
completed = _run_argv(argv, root, env=env)
|
|
1126
1152
|
return completed.returncode, completed.stdout + (
|
|
1127
1153
|
"\n" + completed.stderr if completed.stderr else ""
|
|
1128
1154
|
)
|
|
@@ -1640,17 +1666,7 @@ def _run_routes_drift(root: pathlib.Path) -> tuple[int, str]:
|
|
|
1640
1666
|
problem = _node_modules_problem(root, "frontend")
|
|
1641
1667
|
if problem is not None:
|
|
1642
1668
|
return 1, problem
|
|
1643
|
-
|
|
1644
|
-
executable = shutil.which(argv[0]) or argv[0]
|
|
1645
|
-
completed = subprocess.run( # noqa: S603 - fixed manifest argv, shell=False
|
|
1646
|
-
[executable, *argv[1:]],
|
|
1647
|
-
cwd=root,
|
|
1648
|
-
capture_output=True,
|
|
1649
|
-
text=True,
|
|
1650
|
-
encoding="utf-8",
|
|
1651
|
-
errors="replace",
|
|
1652
|
-
check=False,
|
|
1653
|
-
)
|
|
1669
|
+
completed = _run_argv(routes_argv(check=True), root)
|
|
1654
1670
|
return completed.returncode, f"{completed.stdout}{completed.stderr}"
|
|
1655
1671
|
|
|
1656
1672
|
|
|
@@ -1663,21 +1679,6 @@ _API_DOCS_ARTIFACTS = ("platform-api.md", "terp_core.pyi")
|
|
|
1663
1679
|
_API_DOCS_ADOPT_HINT = "commit docs/platform-api.md + docs/terp_core.pyi to enable"
|
|
1664
1680
|
|
|
1665
1681
|
|
|
1666
|
-
def _tracked(root: pathlib.Path, relative: str) -> bool:
|
|
1667
|
-
"""Whether *relative* is a file git is tracking in *root*."""
|
|
1668
|
-
git = shutil.which("git") or "git"
|
|
1669
|
-
completed = subprocess.run( # noqa: S603 - fixed argv, shell=False
|
|
1670
|
-
[git, "ls-files", "--error-unmatch", "--", relative],
|
|
1671
|
-
cwd=root,
|
|
1672
|
-
capture_output=True,
|
|
1673
|
-
text=True,
|
|
1674
|
-
encoding="utf-8",
|
|
1675
|
-
errors="replace",
|
|
1676
|
-
check=False,
|
|
1677
|
-
)
|
|
1678
|
-
return completed.returncode == 0
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
1682
|
def _run_api_docs_drift(root: pathlib.Path) -> tuple[int, str]:
|
|
1682
1683
|
"""Regenerate the API reference and fail on drift from the committed copy.
|
|
1683
1684
|
|
|
@@ -1707,7 +1708,20 @@ def _run_api_docs_drift(root: pathlib.Path) -> tuple[int, str]:
|
|
|
1707
1708
|
f"({_API_DOCS_ADOPT_HINT})",
|
|
1708
1709
|
)
|
|
1709
1710
|
relatives = [f"docs/{name}" for name in _API_DOCS_ARTIFACTS]
|
|
1710
|
-
|
|
1711
|
+
probes = {
|
|
1712
|
+
relative: _run_argv(["git", "ls-files", "--error-unmatch", "--", relative], root)
|
|
1713
|
+
for relative in relatives
|
|
1714
|
+
}
|
|
1715
|
+
if any(probe.returncode == _NOT_FOUND for probe in probes.values()):
|
|
1716
|
+
# Not "untracked, skipped": without git there is no answer to whether the pair is
|
|
1717
|
+
# tracked, and reading that silence as "no" would turn this check green over a
|
|
1718
|
+
# comparison it never made.
|
|
1719
|
+
return (
|
|
1720
|
+
_NOT_FOUND,
|
|
1721
|
+
"git: executable not found on PATH - the drift check asks git whether "
|
|
1722
|
+
f"{' + '.join(relatives)} are tracked, so it cannot run without it",
|
|
1723
|
+
)
|
|
1724
|
+
tracked = [relative for relative, probe in probes.items() if probe.returncode == 0]
|
|
1711
1725
|
if not tracked:
|
|
1712
1726
|
return (
|
|
1713
1727
|
0,
|
|
@@ -1729,16 +1743,7 @@ def _run_api_docs_drift(root: pathlib.Path) -> tuple[int, str]:
|
|
|
1729
1743
|
written = [str(path) for path in api_docs(str(docs))]
|
|
1730
1744
|
finally:
|
|
1731
1745
|
os.chdir(previous)
|
|
1732
|
-
|
|
1733
|
-
completed = subprocess.run( # noqa: S603 - fixed argv, shell=False
|
|
1734
|
-
[git, "diff", "--exit-code", "--", *relatives],
|
|
1735
|
-
cwd=root,
|
|
1736
|
-
capture_output=True,
|
|
1737
|
-
text=True,
|
|
1738
|
-
encoding="utf-8",
|
|
1739
|
-
errors="replace",
|
|
1740
|
-
check=False,
|
|
1741
|
-
)
|
|
1746
|
+
completed = _run_argv(["git", "diff", "--exit-code", "--", *relatives], root)
|
|
1742
1747
|
output = "\n".join(
|
|
1743
1748
|
["\n".join(f"wrote {path}" for path in written), completed.stdout]
|
|
1744
1749
|
)
|
|
@@ -1941,17 +1946,7 @@ def _run_api_client(root: pathlib.Path) -> tuple[int, str]:
|
|
|
1941
1946
|
return 1, f"could not export the OpenAPI document: {refusal}"
|
|
1942
1947
|
finally:
|
|
1943
1948
|
os.chdir(previous)
|
|
1944
|
-
|
|
1945
|
-
executable = shutil.which(argv[0]) or argv[0]
|
|
1946
|
-
completed = subprocess.run( # noqa: S603 - fixed argv, shell=False
|
|
1947
|
-
[executable, *argv[1:]],
|
|
1948
|
-
cwd=root,
|
|
1949
|
-
capture_output=True,
|
|
1950
|
-
text=True,
|
|
1951
|
-
encoding="utf-8",
|
|
1952
|
-
errors="replace",
|
|
1953
|
-
check=False,
|
|
1954
|
-
)
|
|
1949
|
+
completed = _run_argv(["npm", "--prefix", "frontend", "run", "generate"], root)
|
|
1955
1950
|
output = f"wrote {written.name}\n{completed.stdout}{completed.stderr}"
|
|
1956
1951
|
if completed.returncode != 0:
|
|
1957
1952
|
output += (
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|