dirigent-plugin 0.20.0__tar.gz → 0.22.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.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirigent-plugin
3
- Version: 0.20.0
3
+ Version: 0.22.0
4
4
  Summary: The dirigent plugin contract: block specs, protocols, and markers.
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Programming Language :: Python :: 3.13
9
- Requires-Dist: dirigent-common==0.20.0
9
+ Requires-Dist: dirigent-common==0.22.0
10
10
  Requires-Dist: httpx2>=2.12.0
11
11
  Requires-Dist: pluginkit>=0.5.0
12
12
  Requires-Dist: pydantic>=2.13.5
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-plugin"
3
- version = "0.20.0"
3
+ version = "0.22.0"
4
4
  description = "The dirigent plugin contract: block specs, protocols, and markers."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -11,7 +11,7 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "dirigent-common==0.20.0",
14
+ "dirigent-common==0.22.0",
15
15
  "httpx2>=2.12.0",
16
16
  "pluginkit>=0.5.0",
17
17
  "pydantic>=2.13.5",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-plugin"
3
- version = "0.20.0"
3
+ version = "0.22.0"
4
4
  description = "The dirigent plugin contract: block specs, protocols, and markers."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -11,7 +11,7 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "dirigent-common==0.20.0",
14
+ "dirigent-common==0.22.0",
15
15
  "httpx2>=2.12.0",
16
16
  "pluginkit>=0.5.0",
17
17
  "pydantic>=2.13.5",
@@ -16,7 +16,16 @@ from pydantic import BaseModel, ConfigDict, Field, GetJsonSchemaHandler, JsonVal
16
16
  from pydantic.json_schema import JsonSchemaValue, SkipJsonSchema
17
17
  from pydantic_core import CoreSchema
18
18
 
19
- from dirigent_common import API_VERSION, SHELL_MEDIA_TYPE, BlockModel, HealthReport, Issue, JsonMap, Message
19
+ from dirigent_common import (
20
+ API_VERSION,
21
+ SHELL_MEDIA_TYPE,
22
+ BlockModel,
23
+ Catalogue,
24
+ HealthReport,
25
+ Issue,
26
+ JsonMap,
27
+ Message,
28
+ )
20
29
  from dirigent_plugin.messages import (
21
30
  DUPLICATE_ID,
22
31
  INVALID_MARK,
@@ -280,8 +289,9 @@ class NotYet(BaseModel):
280
289
  worker that dies before that commit leaves the older cursor for the next poke to read
281
290
  from again. So a poke must tolerate reading the same ground twice.
282
291
 
283
- The cursor's life is the waiting attempt. A poke that succeeds ends the step, and
284
- nothing carries the cursor past it.
292
+ The cursor's life is the waiting attempt. A poke that succeeds ends the step, and only
293
+ :meth:`Sensor.resume_cursor` carries a cursor past it: a watch hands what that returns to
294
+ the first poke of the run it arms next.
285
295
  """
286
296
 
287
297
 
@@ -363,7 +373,13 @@ class ContainerResult(BaseModel):
363
373
 
364
374
 
365
375
  class AlertMessage(BaseModel):
366
- """What an alert rule hands a notifier: the event, a rendered summary, and links back."""
376
+ """What an alert rule hands a notifier: the event, a rendered summary, and links back.
377
+
378
+ The subject and the body are the rule author's own template rendered over the run, so they
379
+ are that author's words in that author's language and carry no code. What dirigent itself
380
+ said is ``error_code`` and ``error_params``: the refusal the run ended with, which a
381
+ notifier with a catalogue can render in the language of wherever it delivers.
382
+ """
367
383
 
368
384
  event: str = Field(min_length=1)
369
385
  subject: str = Field(min_length=1)
@@ -371,6 +387,10 @@ class AlertMessage(BaseModel):
371
387
  run_id: RunId | None = None
372
388
  pipeline: str | None = None
373
389
  url: str | None = None
390
+ error_code: str | None = None
391
+ """The dotted code of the refusal the alerted run ended with, when it ended with one."""
392
+ error_params: dict[str, JsonValue] = Field(default_factory=dict)
393
+ """The specifics that refusal rendered, for a re-render in another language."""
374
394
  context: dict[str, JsonValue] = Field(default_factory=dict)
375
395
 
376
396
 
@@ -492,6 +512,10 @@ class RunSnapshot(BaseModel):
492
512
  total_steps: int = Field(default=0, ge=0)
493
513
  finished_steps: int = Field(default=0, ge=0)
494
514
  error: str | None = None
515
+ error_code: str | None = None
516
+ """The dotted code of the refusal this run ended with, when it ended with one."""
517
+ error_params: JsonMap = Field(default_factory=dict)
518
+ """The specifics that refusal rendered, for a re-render in another language."""
495
519
 
496
520
  @property
497
521
  def progress(self) -> float | None:
@@ -523,8 +547,12 @@ class Runs(Protocol):
523
547
  """Describe a run this instance holds, or return None when it holds no such run."""
524
548
  ...
525
549
 
526
- async def cancel(self, run_id: RunId, *, reason: str) -> bool:
527
- """Cancel a run; False means it had already settled and there was nothing to stop."""
550
+ async def cancel(self, run_id: RunId, reason: Message, /, **params: Any) -> bool:
551
+ """Cancel a run; False means it had already settled and there was nothing to stop.
552
+
553
+ The reason is a catalogued message, so the cancelled run carries a code and whoever
554
+ reads it renders their own sentence rather than this block's English.
555
+ """
528
556
  ...
529
557
 
530
558
 
@@ -551,7 +579,9 @@ class StepContext(Protocol):
551
579
  cursor: JsonMap | None
552
580
  """The cursor the last committed :class:`NotYet` returned, and None on the first poke.
553
581
 
554
- Only a sensor's poke reads it; every other call sees None.
582
+ The first poke of a run a watch armed is handed where the watch's last success left off
583
+ instead, which is what :meth:`Sensor.resume_cursor` returned. Only a sensor's poke reads
584
+ it; every other call sees None.
555
585
  """
556
586
 
557
587
  def connection[C: BaseModel](self, ref: ConnectionRef, model: type[C]) -> C:
@@ -682,6 +712,16 @@ class Sensor[ConfigT: BaseModel, OutputT: BaseModel](ABC):
682
712
  """Observe the world once, read-only and briefly; NotYet is not a failure."""
683
713
  ...
684
714
 
715
+ def resume_cursor(self, output: OutputT) -> JsonMap | None:
716
+ """Say where a poke that succeeded left off, read from the output it succeeded with.
717
+
718
+ A watch stores what this returns and hands it to the first poke of the next run it
719
+ arms as ``ctx.cursor``. None, the default, starts every run fresh. It is stored in the
720
+ transaction that settles the step, so it is only as far along as the last success that
721
+ committed, and the next poke may read that ground again.
722
+ """
723
+ return None
724
+
685
725
  def check_config(self, config: BaseModel) -> list[Issue]:
686
726
  """List the extra refusals this block makes at apply, beyond what its schema says.
687
727
 
@@ -824,7 +864,7 @@ type AnySensor = Sensor[Any, Any]
824
864
 
825
865
 
826
866
  class Contribution(BaseModel):
827
- """Everything one plugin adds, across all six surfaces, gathered by the host at startup."""
867
+ """Everything one plugin adds, across all seven surfaces, gathered by the host at startup."""
828
868
 
829
869
  model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True)
830
870
 
@@ -839,6 +879,14 @@ class Contribution(BaseModel):
839
879
  ``format: <name>`` then asserts wherever the contributing pack is installed, and stays a
840
880
  passing annotation on an instance without it."""
841
881
 
882
+ labels: list[Catalogue] = Field(default_factory=list[Catalogue])
883
+ """The message catalogues this plugin refuses out of, so a surface can render its codes.
884
+
885
+ A pack's refusal reaches a browser under the pack's own code, and the browser holds no table
886
+ for it. Contributing the catalogue is what puts one there: the host serves every code and its
887
+ template, and a renderer that knows the code says its own sentence rather than repeating the
888
+ English this process happened to mint."""
889
+
842
890
  @field_validator("api_version")
843
891
  @classmethod
844
892
  def _check_api_version(cls, value: int) -> int:
@@ -855,6 +903,7 @@ class Contribution(BaseModel):
855
903
  _require_unique("notifier id", [notifier.id for notifier in self.notifiers])
856
904
  _require_unique("connection kind id", [connection.id for connection in self.connection_kinds])
857
905
  _require_unique("format", list(self.formats))
906
+ _require_unique("label prefix", [catalogue.prefix for catalogue in self.labels])
858
907
  return self
859
908
 
860
909
  @model_validator(mode="after")
@@ -888,6 +937,7 @@ def merge_contributions(contributions: list[Contribution]) -> Contribution:
888
937
  "storage_backends": [],
889
938
  "notifiers": [],
890
939
  "connection_kinds": [],
940
+ "labels": [],
891
941
  }
892
942
  for contribution in contributions:
893
943
  for surface, collected in merged.items():