dirigent-plugin 0.22.0__tar.gz → 0.23.1__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.22.0
3
+ Version: 0.23.1
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.22.0
9
+ Requires-Dist: dirigent-common==0.23.1
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.22.0"
3
+ version = "0.23.1"
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.22.0",
14
+ "dirigent-common==0.23.1",
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.22.0"
3
+ version = "0.23.1"
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.22.0",
14
+ "dirigent-common==0.23.1",
15
15
  "httpx2>=2.12.0",
16
16
  "pluginkit>=0.5.0",
17
17
  "pydantic>=2.13.5",
@@ -3,6 +3,7 @@
3
3
  from dirigent_plugin.blocks import (
4
4
  BLOCK_ID_PATTERN,
5
5
  MARK_LIMIT,
6
+ REFERENCE_KEYWORD,
6
7
  SHELL_VARIABLE_PREFIX,
7
8
  SHELL_VARIABLES_FIELD,
8
9
  SURFACE_ID_PATTERN,
@@ -45,6 +46,7 @@ from dirigent_plugin.blocks import (
45
46
  classify_default,
46
47
  mark_refusal,
47
48
  merge_contributions,
49
+ reference_fields,
48
50
  shell_string_fields,
49
51
  )
50
52
  from dirigent_plugin.markers import (
@@ -112,6 +114,7 @@ __all__ = [
112
114
  "RunnerEngine",
113
115
  "Runs",
114
116
  "SURFACE_ID_PATTERN",
117
+ "REFERENCE_KEYWORD",
115
118
  "SchemaRef",
116
119
  "Sensor",
117
120
  "SensorSpec",
@@ -136,5 +139,6 @@ __all__ = [
136
139
  "formatters",
137
140
  "mark_refusal",
138
141
  "merge_contributions",
142
+ "reference_fields",
139
143
  "shell_string_fields",
140
144
  ]
@@ -147,6 +147,14 @@ class ShellString:
147
147
  return published
148
148
 
149
149
 
150
+ #: What :class:`Reference` publishes, and what reading one back looks for.
151
+ REFERENCE_KEYWORD: Final = "x-dirigent-ref"
152
+
153
+ #: The only pointer shape a block's own config schema writes, which pydantic emits for a
154
+ #: field whose type is a named alias.
155
+ _LOCAL_DEFS: Final = "#/$defs/"
156
+
157
+
150
158
  class Reference:
151
159
  """Marks a config field whose value is the code of another thing this instance holds.
152
160
 
@@ -173,7 +181,7 @@ class Reference:
173
181
  ) -> JsonSchemaValue:
174
182
  """Publish what the marked field's code names, leaving what it validates untouched."""
175
183
  published = handler(schema)
176
- published["x-dirigent-ref"] = self.kind
184
+ published[REFERENCE_KEYWORD] = self.kind
177
185
  return published
178
186
 
179
187
 
@@ -184,6 +192,55 @@ type ConnectionRef = Annotated[str, Reference("connection")]
184
192
  type SchemaRef = Annotated[str, Reference("schema")]
185
193
 
186
194
 
195
+ def _pointed_at(shape: dict[str, Any], pool: dict[str, Any]) -> dict[str, Any] | None:
196
+ """The definition a local ``$ref`` names, or nothing where it names none."""
197
+ pointer = shape.get("$ref")
198
+ if not isinstance(pointer, str) or not pointer.startswith(_LOCAL_DEFS):
199
+ return None
200
+ target = pool.get(pointer.removeprefix(_LOCAL_DEFS))
201
+ return cast("dict[str, Any]", target) if isinstance(target, dict) else None
202
+
203
+
204
+ def _shapes(shape: dict[str, Any], pool: dict[str, Any]) -> list[dict[str, Any]]:
205
+ """One field's own shape, the definition it points at, and each branch of a union."""
206
+ found: list[dict[str, Any]] = [shape]
207
+ target = _pointed_at(shape, pool)
208
+ if target is not None:
209
+ found.append(target)
210
+ for option in cast("list[Any]", shape.get("anyOf") or []):
211
+ if not isinstance(option, dict):
212
+ continue
213
+ branch = cast("dict[str, Any]", option)
214
+ found.append(branch)
215
+ pointed = _pointed_at(branch, pool)
216
+ if pointed is not None:
217
+ found.append(pointed)
218
+ return found
219
+
220
+
221
+ def reference_fields(schema: JsonMap, kind: str) -> set[str]:
222
+ """Name the config fields a block publishes as references to one kind of thing.
223
+
224
+ A block says which of its strings hold a code, so nothing here guesses from a value: a
225
+ code and a value of the same shape are not told apart by looking. The marker rides on the
226
+ field's own shape or on the definition that shape points at, because a field typed as a
227
+ named alias is published as a ``$ref``.
228
+ """
229
+ properties = schema.get("properties")
230
+ if not isinstance(properties, dict):
231
+ return set()
232
+ held = schema.get("$defs")
233
+ pool = cast("dict[str, Any]", held) if isinstance(held, dict) else {}
234
+ named: set[str] = set()
235
+ for field, declared in cast("dict[str, Any]", properties).items():
236
+ if not isinstance(declared, dict):
237
+ continue
238
+ shapes = _shapes(cast("dict[str, Any]", declared), pool)
239
+ if any(one.get(REFERENCE_KEYWORD) == kind for one in shapes):
240
+ named.add(str(field))
241
+ return named
242
+
243
+
187
244
  #: What the engine names the variables it substitutes a shell string's references out into.
188
245
  SHELL_VARIABLE_PREFIX: Final = "DIRIGENT_V"
189
246
 
@@ -602,7 +659,12 @@ class StepContext(Protocol):
602
659
  ...
603
660
 
604
661
  def schema(self, code: str) -> JsonMap:
605
- """Resolve a named JSON Schema the instance holds by code; an unknown code fails the step."""
662
+ """Resolve one of the JSON Schemas this run holds by code; any other code fails the step.
663
+
664
+ A run holds the body of every schema its document named when it was created, so a
665
+ stored schema edited mid-run does not change what the rest of that run checks
666
+ against. A code the document does not name is not one the run holds.
667
+ """
606
668
  ...
607
669
 
608
670
  def format_checker(self) -> FormatChecker: