terp-cli 0.36.1__tar.gz → 0.37.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.36.1 → terp_cli-0.37.0}/PKG-INFO +9 -9
- {terp_cli-0.36.1 → terp_cli-0.37.0}/pyproject.toml +9 -9
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/__init__.py +57 -2
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/service_accounts.py +5 -5
- {terp_cli-0.36.1 → terp_cli-0.37.0}/.gitignore +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/signer/Dockerfile +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/signer/terp-arch-releases.json +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/_appref.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/_engine.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/_output.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/_subjects.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/access.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/api_contract.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/api_contract_bounds.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/api_contract_check.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/api_contract_schemas.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/api_contract_shapes.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/apidocs.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/authz_surface.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/capabilities.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deploy_safety.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deployment_adopt.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deployment_app.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deployment_check.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deployment_declare.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deployment_render.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/dev.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/docker.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/docker_masks.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/envfile.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/envrequired.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/envschema.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/envseams.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/fmt.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/grants.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/heartbeat.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/jobs.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/leases.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/locks.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/module_roles.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/openapi.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/outbox.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/ports.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/profiles.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/py.typed +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/__init__.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_common.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_compare.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_dockerfile.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_git.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_image.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_locks.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_plan.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_registry.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_render.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_sandbox.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/build.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/cli.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/dsse.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/gate.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/manifest.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/promote.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/sign.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/reseal.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/routes.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/scaffold.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/schema.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/seed.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/smoke.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/users.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/verify.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/version.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/workbench.py +0 -0
- {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/workbench_adopt.py +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: terp-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.37.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.37.0
|
|
11
|
+
Requires-Dist: terp-core==0.37.0
|
|
12
|
+
Requires-Dist: terp-migrations==0.37.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.37.0; extra == 'jobs'
|
|
15
|
+
Requires-Dist: terp-cap-scheduler-apscheduler==0.37.0; extra == 'jobs'
|
|
16
16
|
Provides-Extra: release
|
|
17
17
|
Requires-Dist: cryptography>=46; extra == 'release'
|
|
18
18
|
Requires-Dist: packaging>=24; extra == 'release'
|
|
19
|
-
Requires-Dist: terp-cap-egress==0.
|
|
19
|
+
Requires-Dist: terp-cap-egress==0.37.0; extra == 'release'
|
|
20
20
|
Provides-Extra: scheduler
|
|
21
|
-
Requires-Dist: terp-cap-scheduler-apscheduler==0.
|
|
21
|
+
Requires-Dist: terp-cap-scheduler-apscheduler==0.37.0; extra == 'scheduler'
|
|
22
22
|
Provides-Extra: worker
|
|
23
|
-
Requires-Dist: terp-cap-outbox==0.
|
|
23
|
+
Requires-Dist: terp-cap-outbox==0.37.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.37.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.37.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.37.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.37.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.37.0"]
|
|
33
|
+
scheduler = ["terp-cap-scheduler-apscheduler==0.37.0"]
|
|
34
34
|
jobs = [
|
|
35
|
-
"terp-cap-outbox==0.
|
|
36
|
-
"terp-cap-scheduler-apscheduler==0.
|
|
35
|
+
"terp-cap-outbox==0.37.0",
|
|
36
|
+
"terp-cap-scheduler-apscheduler==0.37.0",
|
|
37
37
|
]
|
|
38
38
|
# `terp release` and `terp promote` (ADR 0187): the DSSE signature (cryptography), the lock's
|
|
39
39
|
# markers (packaging), and the one declared way to a registry, an index and the vulnerability
|
|
@@ -42,7 +42,7 @@ jobs = [
|
|
|
42
42
|
release = [
|
|
43
43
|
"cryptography>=46",
|
|
44
44
|
"packaging>=24",
|
|
45
|
-
"terp-cap-egress==0.
|
|
45
|
+
"terp-cap-egress==0.37.0",
|
|
46
46
|
]
|
|
47
47
|
|
|
48
48
|
[project.scripts]
|
|
@@ -82,21 +82,24 @@ _GUIDE_TOPICS: dict[str, str] = {
|
|
|
82
82
|
Add a module (the "10-minute module")
|
|
83
83
|
|
|
84
84
|
1) models.py table model (inherit BaseTable; never redeclare id/created_at/version)
|
|
85
|
-
from
|
|
85
|
+
from datetime import datetime
|
|
86
|
+
from terp.core import BaseTable, UtcDateTime
|
|
86
87
|
from sqlmodel import Field
|
|
87
88
|
class Invoice(BaseTable, table=True):
|
|
88
89
|
number: str = Field(max_length=50, index=True, unique=True)
|
|
89
90
|
amount_cents: int
|
|
91
|
+
issued_at: datetime = Field(sa_type=UtcDateTime()) # an instant, stored as UTC
|
|
90
92
|
|
|
91
93
|
2) schemas.py DTOs (cap every input string; the Read DTO is what the API returns)
|
|
92
94
|
from terp.core import BaseSchema, BaseUpdateSchema
|
|
93
95
|
class InvoiceCreate(BaseSchema):
|
|
94
96
|
number: str = Field(max_length=50)
|
|
95
97
|
amount_cents: int
|
|
98
|
+
issued_at: datetime
|
|
96
99
|
class InvoiceUpdate(BaseUpdateSchema): # `version` is required (optimistic concurrency)
|
|
97
100
|
amount_cents: int | None = None
|
|
98
101
|
class InvoiceRead(BaseSchema): # NOT the table model
|
|
99
|
-
id: uuid.UUID; number: str; amount_cents: int; version: int
|
|
102
|
+
id: uuid.UUID; number: str; amount_cents: int; issued_at: datetime; version: int
|
|
100
103
|
|
|
101
104
|
3) service.py business logic (CRUD is inherited and audited)
|
|
102
105
|
from terp.core import BaseService
|
|
@@ -998,6 +1001,58 @@ Object-level (per-row) authorization (OwnedMixin)
|
|
|
998
1001
|
apply_object_authz does not restrict a row with no ownership trait, so the job body
|
|
999
1002
|
itself would run fine. The clause is a judgement about the DESIGN, enforced before the
|
|
1000
1003
|
app serves anything -- not a description of what the write boundary would do.
|
|
1004
|
+
""",
|
|
1005
|
+
"aggregates": """\
|
|
1006
|
+
Aggregates: counts and sums declared on the service, like filters (ADR 0204)
|
|
1007
|
+
|
|
1008
|
+
- The problem: a statistics screen counts and sums the rows its lists read. A hand-built
|
|
1009
|
+
select(func.count(...)).group_by(...) over a soft-delete or tenant model is what
|
|
1010
|
+
reads_use_base_query refuses, and the workaround (an aggregate over a subquery of
|
|
1011
|
+
base_query()) makes every module solve calendar days, bounds and grouping on its own.
|
|
1012
|
+
- Declare what may be counted and summed, and by what, beside filterable and sortable:
|
|
1013
|
+
from terp.core import Dimension, FilterField, Measure, TimeBucket
|
|
1014
|
+
class InvoiceService(BaseService[Invoice, InvoiceCreate, InvoiceUpdate]):
|
|
1015
|
+
model = Invoice
|
|
1016
|
+
filterable = (FilterField("customer_id", Invoice.customer_id),)
|
|
1017
|
+
measures = (Measure("invoices"), Measure("revenue", Invoice.amount, op="sum"))
|
|
1018
|
+
dimensions = (Dimension("status", Invoice.status),)
|
|
1019
|
+
time_buckets = (TimeBucket("issued", Invoice.issued_at, steps=("day", "month")),)
|
|
1020
|
+
op is one of count, count_distinct, sum, avg, min, max. A dimension groups by a
|
|
1021
|
+
column's own values; an expression is a column the model declares, or a database view.
|
|
1022
|
+
- The route takes the capped query and returns a page typed by the declarations:
|
|
1023
|
+
from terp.core import Page, SessionDep, StatisticsQueryDep
|
|
1024
|
+
@router.get("/statistics", response_model=Page[InvoiceService.statistics_model()])
|
|
1025
|
+
def invoice_statistics(query: StatisticsQueryDep, session: SessionDep, customer_id: uuid.UUID | None = None):
|
|
1026
|
+
return InvoiceService().aggregate_page(session, query, filters={"customer_id": customer_id})
|
|
1027
|
+
The caller asks ?measures=invoices&measures=revenue&by=status&sort=-revenue, and for a
|
|
1028
|
+
series &bucket=issued&start=2026-01-01&end=2026-12-31&zone=Europe/Amsterdam&step=month.
|
|
1029
|
+
- Each group carries its dimensions, its measures and, with a step, its series: one point
|
|
1030
|
+
per calendar step of the zone, empty steps counting zero, each with its local date (label
|
|
1031
|
+
a step by date, not by re-zoning start). total counts the groups, so "the top eight" is
|
|
1032
|
+
limit=8 and the rest are still counted. A Numeric sum arrives as a decimal string.
|
|
1033
|
+
- A day is the zone's own, so a day that daylight saving shortens keeps its real length. A
|
|
1034
|
+
week starts on Monday; a series may start or end part way through a week or month.
|
|
1035
|
+
- Everything undeclared is refused with a 400 that lists what is declared: a measure, a
|
|
1036
|
+
dimension, a time bucket, a step, a sort key, a filter. A page holds at most max_groups
|
|
1037
|
+
(100); a series at most the bucket's max_buckets (366); a page at most max_points (10,000)
|
|
1038
|
+
groups times steps. Each cap refuses, never clamps, and names what would fit.
|
|
1039
|
+
- Personal data stays personal. A Dimension or a TimeBucket over a field declared
|
|
1040
|
+
Personal(...), and a sum, avg, min or max over one, is refused when the class is defined:
|
|
1041
|
+
over a group of one each is that person's value. count and count_distinct may run over
|
|
1042
|
+
personal data. A statistic is never narrowed by a filter over a personal field (counts
|
|
1043
|
+
under a contains filter would spell out an address); the list keeps its filter.
|
|
1044
|
+
- A read that is not a service's (a capability's own table) uses the same seam directly:
|
|
1045
|
+
from terp.core import Statistics, aggregate, group_model
|
|
1046
|
+
STATS = Statistics(measures=(Measure("events"),), dimensions=(...,), time_buckets=(...,))
|
|
1047
|
+
groups, total = aggregate(session, select(AuditEvent), STATS, measures=["events"], ...)
|
|
1048
|
+
- The runtime half: the request session scopes every ORM read, aggregates included, and
|
|
1049
|
+
refuses with UnscopedReadError (a 500) a SELECT that reads a row-scoped table through Core
|
|
1050
|
+
- Model.__table__.c.x, a Core join or subquery over it, a Core alias, table("name"),
|
|
1051
|
+
literal SQL in a FROM - because there is no mapped entity in that SELECT to attach the
|
|
1052
|
+
scope to. A many-to-many whose link table is row-scoped is refused the same way: map the
|
|
1053
|
+
link as an association object.
|
|
1054
|
+
- On SQLite a timestamp is stored as its wall time without its offset: write instants in UTC
|
|
1055
|
+
(every timestamp the framework stamps already is) and the steps are exact on both dialects.
|
|
1001
1056
|
""",
|
|
1002
1057
|
"leases": """\
|
|
1003
1058
|
Leases: expiring, fenced custody of work (leases capability)
|
|
@@ -97,12 +97,12 @@ def create_service_account_command(
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def _as_utc(moment: datetime.datetime) -> datetime.datetime:
|
|
100
|
-
"""A
|
|
100
|
+
"""A timestamp as aware UTC; a naive one is taken as UTC.
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
A naive/aware comparison raises rather than answers. The account's timestamps are
|
|
103
|
+
``UtcDateTime`` columns (ADR 0205), so a value read back from them is aware already
|
|
104
|
+
and this changes nothing for it; it keeps a naive value from anywhere else from
|
|
105
|
+
raising.
|
|
106
106
|
"""
|
|
107
107
|
return moment if moment.tzinfo is not None else moment.replace(tzinfo=datetime.UTC)
|
|
108
108
|
|
|
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
|
|
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
|