terp-cli 0.5.0__tar.gz → 0.5.2__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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: terp-cli
3
+ Version: 0.5.2
4
+ Summary: Terp command-line tool — inspect, scaffolding, migrations, checks, api-docs.
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: terp-arch==0.5.2
8
+ Requires-Dist: terp-core==0.5.2
9
+ Requires-Dist: terp-migrations==0.5.2
10
+ Provides-Extra: jobs
11
+ Requires-Dist: terp-cap-outbox==0.5.2; extra == 'jobs'
12
+ Requires-Dist: terp-cap-scheduler-apscheduler==0.5.2; extra == 'jobs'
13
+ Provides-Extra: scheduler
14
+ Requires-Dist: terp-cap-scheduler-apscheduler==0.5.2; extra == 'scheduler'
15
+ Provides-Extra: worker
16
+ Requires-Dist: terp-cap-outbox==0.5.2; extra == 'worker'
@@ -4,29 +4,29 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "terp-cli"
7
- version = "0.5.0"
7
+ version = "0.5.2"
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.5.0",
12
+ "terp-core==0.5.2",
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.5.0",
15
+ "terp-migrations==0.5.2",
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.5.0",
18
+ "terp-arch==0.5.2",
19
19
  ]
20
20
 
21
21
  # Job-process adapters stay optional: the base CLI must not install the table-owning outbox
22
22
  # or a scheduler engine into apps that only use inspect/check/migrate. Deployments can select
23
23
  # one process role, while `jobs` is the convenient complete jobs-process bundle.
24
24
  [project.optional-dependencies]
25
- worker = ["terp-cap-outbox==0.5.0"]
26
- scheduler = ["terp-cap-scheduler-apscheduler==0.5.0"]
25
+ worker = ["terp-cap-outbox==0.5.2"]
26
+ scheduler = ["terp-cap-scheduler-apscheduler==0.5.2"]
27
27
  jobs = [
28
- "terp-cap-outbox==0.5.0",
29
- "terp-cap-scheduler-apscheduler==0.5.0",
28
+ "terp-cap-outbox==0.5.2",
29
+ "terp-cap-scheduler-apscheduler==0.5.2",
30
30
  ]
31
31
 
32
32
  [project.scripts]
@@ -337,15 +337,60 @@ Password strength (PasswordPolicy, Tier-B)
337
337
  "events": """\
338
338
  Domain events (eventbus capability)
339
339
 
340
+ - The capability is a separate distribution — nothing below exists until you add it:
341
+ uv add terp-cap-eventbus
342
+ Then import from terp.capabilities.eventbus (EventEmittingService, LifecycleEventMap,
343
+ subscribe, dispatch_in_process).
340
344
  - Declare typed events in your control plane (never bare strings):
341
345
  NOTE_CREATED = EventDefinition("note.created", payload_schema=NoteCreatedPayload)
342
346
  event_catalog = EventCatalog([NOTE_CREATED])
347
+ - Declare what the module produces in its manifest. The emits list is the module's
348
+ published contract, so the gate refuses an event you emit without naming it here:
349
+ module = ModuleSpec(name="notes", emits=[NOTE_CREATED, NOTE_ARCHIVED])
343
350
  - Emit declaratively from a service (atomic with the write):
344
351
  class NoteService(EventEmittingService[Note, NoteCreate, NoteUpdate]):
345
352
  model = Note
346
353
  event_map = LifecycleEventMap(created=NOTE_CREATED)
354
+ - The map covers "every write of this shape emits this event". When the decision is
355
+ CONDITIONAL — a state transition, one event for some updates and none for others —
356
+ extend the same hook rather than emitting from a router or a task:
357
+ class NoteService(EventEmittingService[Note, NoteCreate, NoteUpdate]):
358
+ model = Note
359
+ event_map = LifecycleEventMap(created=NOTE_CREATED)
360
+
361
+ def _after_write(self, session, entity, action) -> None:
362
+ super()._after_write(session, entity, action)
363
+ if action is AuditAction.UPDATED and entity.status == "archived":
364
+ emit(session, event=NOTE_ARCHIVED, payload=NoteArchivedPayload(id=entity.id))
365
+ _after_write runs inside the write's transaction, so a conditional emit is still
366
+ atomic with the row it describes — which is the whole reason the map lives there.
347
367
  - Subscribe with @subscribe(NOTE_CREATED). Reference catalog constants only (the gate
348
368
  enforces no-drift). Wire create_app(..., event_dispatcher=dispatch_in_process).
369
+ - Switching the bus on in a service-level test: the `terp_events` fixture, never a
370
+ direct configure_events (see `terp guide testing`).
371
+ """,
372
+ "testing": """\
373
+ Testing a Terp app (process-global runtime isolation)
374
+
375
+ - create_app installs SIX process globals per app: the audit policy+sink, the event
376
+ catalog+dispatcher, the job catalog+queue, the schedule catalog, the password policy,
377
+ and the decrypt call site. In a test process they outlive the app that installed them.
378
+ - You get isolation for free. terp-core ships a pytest plugin (terp.core.testing,
379
+ registered under pytest11) whose autouse fixture SNAPSHOTS every runtime before a
380
+ test and RESTORES it after. No conftest.py line, no opt-in. Without it a suite goes
381
+ order-dependent: green together, red alone - the sharpest failure mode there is,
382
+ because the green is the wrong answer.
383
+ - Isolation does not INSTALL a runtime your test needs - that is your decision:
384
+ * whole runtime -> compose the app in a fixture (see apps/example/tests/conftest.py,
385
+ where db_engine + app_db are function-scoped and build() runs per test)
386
+ * only the event bus -> the `terp_events` fixture, which installs a catalog
387
+ (and dispatcher) for the duration of one test:
388
+ terp_events(event_catalog, dispatcher=dispatch_in_process)
389
+ * asserting on what was written to a FAKE/bare session -> `terp_default_runtime`,
390
+ which states the baseline instead of assuming it. Without it a composed app's
391
+ durable audit sink quietly adds audit rows to the session under assertion.
392
+ - If a test passes in the suite and fails alone (or vice versa), it is reading a
393
+ runtime it never installed. Install it explicitly; never re-order the suite.
349
394
  """,
350
395
  "jobs": """\
351
396
  Background jobs (terp.core.enqueue + JobCatalog)
@@ -270,6 +270,19 @@ def _node_platform() -> tuple[str, str]:
270
270
  return system, arch
271
271
 
272
272
 
273
+ def _node_libc(system: str) -> str | None:
274
+ """This machine's libc flavour as npm names it, or None where npm ignores it.
275
+
276
+ npm gates Linux binaries on ``libc`` as well as ``os``/``cpu``: a x64 Linux
277
+ bundler ships a ``-gnu`` *and* a ``-musl`` binding and installs exactly one.
278
+ Reading only ``os``/``cpu`` therefore reports the flavour this machine is not,
279
+ turning a perfectly healthy tree red.
280
+ """
281
+ if system != "linux":
282
+ return None
283
+ return "musl" if any(pathlib.Path("/lib").glob("ld-musl-*.so.1")) else "glibc"
284
+
285
+
273
286
  def _node_modules_problem(root: pathlib.Path) -> str | None:
274
287
  """Explain an unusable ``frontend/node_modules``, or None if it looks fine.
275
288
 
@@ -302,17 +315,19 @@ def _node_modules_problem(root: pathlib.Path) -> str | None:
302
315
  return None
303
316
 
304
317
  system, arch = _node_platform()
318
+ libc = _node_libc(system)
305
319
  missing = [
306
320
  name
307
321
  for name, entry in packages.items()
308
322
  if name.startswith("node_modules/")
309
323
  and isinstance(entry, dict)
310
- # Only packages this platform is *supposed* to have: an entry with no os/cpu
311
- # constraint is platform-neutral, and one constrained elsewhere is absent by
312
- # design rather than by a bad install.
324
+ # Only packages this platform is *supposed* to have: an entry with no
325
+ # os/cpu/libc constraint is platform-neutral, and one constrained elsewhere
326
+ # is absent by design rather than by a bad install.
313
327
  and system in (entry.get("os") or [system])
314
328
  and arch in (entry.get("cpu") or [arch])
315
- and (entry.get("os") or entry.get("cpu"))
329
+ and (libc is None or libc in (entry.get("libc") or [libc]))
330
+ and (entry.get("os") or entry.get("cpu") or entry.get("libc"))
316
331
  and not (frontend / name).exists()
317
332
  ]
318
333
  if not missing:
terp_cli-0.5.0/PKG-INFO DELETED
@@ -1,16 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: terp-cli
3
- Version: 0.5.0
4
- Summary: Terp command-line tool — inspect, scaffolding, migrations, checks, api-docs.
5
- License-Expression: Apache-2.0
6
- Requires-Python: >=3.13
7
- Requires-Dist: terp-arch==0.5.0
8
- Requires-Dist: terp-core==0.5.0
9
- Requires-Dist: terp-migrations==0.5.0
10
- Provides-Extra: jobs
11
- Requires-Dist: terp-cap-outbox==0.5.0; extra == 'jobs'
12
- Requires-Dist: terp-cap-scheduler-apscheduler==0.5.0; extra == 'jobs'
13
- Provides-Extra: scheduler
14
- Requires-Dist: terp-cap-scheduler-apscheduler==0.5.0; extra == 'scheduler'
15
- Provides-Extra: worker
16
- Requires-Dist: terp-cap-outbox==0.5.0; extra == 'worker'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes