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.
Files changed (74) hide show
  1. {terp_cli-0.36.1 → terp_cli-0.37.0}/PKG-INFO +9 -9
  2. {terp_cli-0.36.1 → terp_cli-0.37.0}/pyproject.toml +9 -9
  3. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/__init__.py +57 -2
  4. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/service_accounts.py +5 -5
  5. {terp_cli-0.36.1 → terp_cli-0.37.0}/.gitignore +0 -0
  6. {terp_cli-0.36.1 → terp_cli-0.37.0}/signer/Dockerfile +0 -0
  7. {terp_cli-0.36.1 → terp_cli-0.37.0}/signer/terp-arch-releases.json +0 -0
  8. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/_appref.py +0 -0
  9. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/_engine.py +0 -0
  10. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/_output.py +0 -0
  11. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/_subjects.py +0 -0
  12. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/access.py +0 -0
  13. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/api_contract.py +0 -0
  14. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/api_contract_bounds.py +0 -0
  15. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/api_contract_check.py +0 -0
  16. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/api_contract_schemas.py +0 -0
  17. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/api_contract_shapes.py +0 -0
  18. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/apidocs.py +0 -0
  19. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/authz_surface.py +0 -0
  20. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/capabilities.py +0 -0
  21. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deploy_safety.py +0 -0
  22. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deployment_adopt.py +0 -0
  23. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deployment_app.py +0 -0
  24. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deployment_check.py +0 -0
  25. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deployment_declare.py +0 -0
  26. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/deployment_render.py +0 -0
  27. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/dev.py +0 -0
  28. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/docker.py +0 -0
  29. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/docker_masks.py +0 -0
  30. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/envfile.py +0 -0
  31. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/envrequired.py +0 -0
  32. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/envschema.py +0 -0
  33. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/envseams.py +0 -0
  34. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/fmt.py +0 -0
  35. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/grants.py +0 -0
  36. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/heartbeat.py +0 -0
  37. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/jobs.py +0 -0
  38. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/leases.py +0 -0
  39. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/locks.py +0 -0
  40. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/module_roles.py +0 -0
  41. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/openapi.py +0 -0
  42. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/outbox.py +0 -0
  43. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/ports.py +0 -0
  44. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/profiles.py +0 -0
  45. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/py.typed +0 -0
  46. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/__init__.py +0 -0
  47. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_common.py +0 -0
  48. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_compare.py +0 -0
  49. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_dockerfile.py +0 -0
  50. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_git.py +0 -0
  51. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_image.py +0 -0
  52. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_locks.py +0 -0
  53. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_plan.py +0 -0
  54. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_registry.py +0 -0
  55. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_render.py +0 -0
  56. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/_sandbox.py +0 -0
  57. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/build.py +0 -0
  58. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/cli.py +0 -0
  59. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/dsse.py +0 -0
  60. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/gate.py +0 -0
  61. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/manifest.py +0 -0
  62. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/promote.py +0 -0
  63. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/release/sign.py +0 -0
  64. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/reseal.py +0 -0
  65. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/routes.py +0 -0
  66. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/scaffold.py +0 -0
  67. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/schema.py +0 -0
  68. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/seed.py +0 -0
  69. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/smoke.py +0 -0
  70. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/users.py +0 -0
  71. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/verify.py +0 -0
  72. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/version.py +0 -0
  73. {terp_cli-0.36.1 → terp_cli-0.37.0}/src/terp/cli/workbench.py +0 -0
  74. {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.36.1
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.36.1
11
- Requires-Dist: terp-core==0.36.1
12
- Requires-Dist: terp-migrations==0.36.1
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.36.1; extra == 'jobs'
15
- Requires-Dist: terp-cap-scheduler-apscheduler==0.36.1; extra == 'jobs'
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.36.1; extra == 'release'
19
+ Requires-Dist: terp-cap-egress==0.37.0; extra == 'release'
20
20
  Provides-Extra: scheduler
21
- Requires-Dist: terp-cap-scheduler-apscheduler==0.36.1; extra == 'scheduler'
21
+ Requires-Dist: terp-cap-scheduler-apscheduler==0.37.0; extra == 'scheduler'
22
22
  Provides-Extra: worker
23
- Requires-Dist: terp-cap-outbox==0.36.1; extra == 'worker'
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.36.1"
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.36.1",
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.36.1",
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.36.1",
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.36.1"]
33
- scheduler = ["terp-cap-scheduler-apscheduler==0.36.1"]
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.1",
36
- "terp-cap-scheduler-apscheduler==0.36.1",
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.36.1",
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 terp.core import BaseTable
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 stored timestamp as aware UTC.
100
+ """A timestamp as aware UTC; a naive one is taken as UTC.
101
101
 
102
- PostgreSQL round-trips a timezone-aware column aware; SQLite returns it NAIVE, and a
103
- naive/aware comparison raises rather than answers. The service resolves the same
104
- ambiguity the same way for its expiry check — a naive stamp is UTC, because UTC is
105
- the only thing this capability ever writes.
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