bigraph-schema 1.0.0__py3-none-any.whl
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.
- bigraph_schema/__init__.py +112 -0
- bigraph_schema/core.py +759 -0
- bigraph_schema/edge.py +138 -0
- bigraph_schema/methods/__init__.py +12 -0
- bigraph_schema/methods/apply.py +276 -0
- bigraph_schema/methods/check.py +213 -0
- bigraph_schema/methods/default.py +204 -0
- bigraph_schema/methods/generalize.py +309 -0
- bigraph_schema/methods/handle_parameters.py +182 -0
- bigraph_schema/methods/infer.py +217 -0
- bigraph_schema/methods/jump.py +432 -0
- bigraph_schema/methods/merge.py +405 -0
- bigraph_schema/methods/realize.py +527 -0
- bigraph_schema/methods/resolve.py +692 -0
- bigraph_schema/methods/serialize.py +491 -0
- bigraph_schema/methods/validate.py +249 -0
- bigraph_schema/package/__init__.py +1 -0
- bigraph_schema/package/discover.py +122 -0
- bigraph_schema/parse.py +183 -0
- bigraph_schema/protocols.py +57 -0
- bigraph_schema/schema.py +370 -0
- bigraph_schema/units.py +133 -0
- bigraph_schema-1.0.0.dist-info/METADATA +66 -0
- bigraph_schema-1.0.0.dist-info/RECORD +28 -0
- bigraph_schema-1.0.0.dist-info/WHEEL +5 -0
- bigraph_schema-1.0.0.dist-info/licenses/AUTHORS.md +6 -0
- bigraph_schema-1.0.0.dist-info/licenses/LICENSE +201 -0
- bigraph_schema-1.0.0.dist-info/top_level.txt +1 -0
bigraph_schema/core.py
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bigraph-Schema Core
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
This module defines the **Core** class — the main operational interface for
|
|
6
|
+
`bigraph-schema`. Core manages the translation between *compiled*
|
|
7
|
+
(dataclass-based) and *encoded* (JSON-compatible) representations of both
|
|
8
|
+
**schemas** and **states**.
|
|
9
|
+
|
|
10
|
+
Core provides a consistent API for all major transformations:
|
|
11
|
+
- `access` / `render`: parse and serialize schema definitions
|
|
12
|
+
- `default` / `infer`: connect schemas to example states
|
|
13
|
+
- `serialize` / `realize`: encode and decode state data
|
|
14
|
+
|
|
15
|
+
These methods form a reversible, type-aware layer for schema construction,
|
|
16
|
+
validation, and data transformation.
|
|
17
|
+
|
|
18
|
+
`CoreVisitor` implements the parsing backend, converting textual bigraph
|
|
19
|
+
expressions into structured schema nodes (`Union`, `Tuple`, `Array`, `Link`, etc.).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
import copy
|
|
24
|
+
import typing
|
|
25
|
+
import inspect
|
|
26
|
+
|
|
27
|
+
from pprint import pformat as pf
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
from numpy import dtype
|
|
31
|
+
import numpy.lib.format as nf
|
|
32
|
+
|
|
33
|
+
import pytest
|
|
34
|
+
import logging
|
|
35
|
+
|
|
36
|
+
from plum import dispatch
|
|
37
|
+
from parsimonious.nodes import NodeVisitor
|
|
38
|
+
from dataclasses import dataclass, is_dataclass, replace
|
|
39
|
+
import importlib.metadata
|
|
40
|
+
|
|
41
|
+
from bigraph_schema.schema import (
|
|
42
|
+
BASE_TYPES,
|
|
43
|
+
resolve_path,
|
|
44
|
+
convert_jump,
|
|
45
|
+
convert_path,
|
|
46
|
+
blank_context,
|
|
47
|
+
Node,
|
|
48
|
+
Union,
|
|
49
|
+
Tuple,
|
|
50
|
+
Boolean,
|
|
51
|
+
Or,
|
|
52
|
+
And,
|
|
53
|
+
Xor,
|
|
54
|
+
Number,
|
|
55
|
+
Integer,
|
|
56
|
+
Float,
|
|
57
|
+
Delta,
|
|
58
|
+
Nonnegative,
|
|
59
|
+
String,
|
|
60
|
+
Enum,
|
|
61
|
+
Wrap,
|
|
62
|
+
Maybe,
|
|
63
|
+
Overwrite,
|
|
64
|
+
List,
|
|
65
|
+
Map,
|
|
66
|
+
Tree,
|
|
67
|
+
Array,
|
|
68
|
+
Key,
|
|
69
|
+
Path,
|
|
70
|
+
Wires,
|
|
71
|
+
Schema,
|
|
72
|
+
Link,
|
|
73
|
+
Jump,
|
|
74
|
+
Star,
|
|
75
|
+
Index,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
from bigraph_schema.parse import visit_expression
|
|
79
|
+
from bigraph_schema.edge import Edge
|
|
80
|
+
from bigraph_schema.methods import (
|
|
81
|
+
reify_schema,
|
|
82
|
+
handle_parameters,
|
|
83
|
+
infer,
|
|
84
|
+
default,
|
|
85
|
+
resolve,
|
|
86
|
+
generalize,
|
|
87
|
+
check,
|
|
88
|
+
validate,
|
|
89
|
+
render,
|
|
90
|
+
serialize,
|
|
91
|
+
realize,
|
|
92
|
+
merge,
|
|
93
|
+
jump,
|
|
94
|
+
traverse,
|
|
95
|
+
apply)
|
|
96
|
+
|
|
97
|
+
from bigraph_schema.package import discover_packages
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def schema_keys(schema):
|
|
101
|
+
keys = []
|
|
102
|
+
for key in schema.__dataclass_fields__:
|
|
103
|
+
if key.startswith('_'):
|
|
104
|
+
keys.append(key)
|
|
105
|
+
return keys
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CoreVisitor(NodeVisitor):
|
|
109
|
+
"""Visitor that converts parsed bigraph expressions into schema node structures.
|
|
110
|
+
|
|
111
|
+
Operates within a `Core` context, mapping grammar constructs
|
|
112
|
+
(unions, merges, type parameters, and defaults) into dataclass-based nodes.
|
|
113
|
+
Handles normalization of nested expressions (e.g. `tuple[int,float]`,
|
|
114
|
+
`link[a:int|b:string]`, `(x:y|z:w)`) into instances of `Union`, `Tuple`,
|
|
115
|
+
or structured dicts.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, operation):
|
|
119
|
+
"""Initialize with the active `Core`."""
|
|
120
|
+
self.operation = operation
|
|
121
|
+
|
|
122
|
+
def visit_expression(self, node, visit):
|
|
123
|
+
"""Top-level entry; returns first child."""
|
|
124
|
+
return visit[0]
|
|
125
|
+
|
|
126
|
+
def visit_union(self, node, visit):
|
|
127
|
+
"""Parse `a~b~c` into a `Union(_options=[a,b,c])`."""
|
|
128
|
+
head = [visit[0]]
|
|
129
|
+
tail = [tree['visit'][1] for tree in visit[1]['visit']]
|
|
130
|
+
return Union(_options=head + tail)
|
|
131
|
+
|
|
132
|
+
def visit_merge(self, node, visit):
|
|
133
|
+
"""Parse `a|b|c`; dicts merge to one mapping, others form a `Tuple`."""
|
|
134
|
+
head = [visit[0]]
|
|
135
|
+
tail = [tree['visit'][1] for tree in visit[1]['visit']]
|
|
136
|
+
nodes = head + tail
|
|
137
|
+
if all(isinstance(tree, dict) for tree in nodes):
|
|
138
|
+
merged = {}
|
|
139
|
+
for tree in nodes:
|
|
140
|
+
merged.update(tree)
|
|
141
|
+
return merged
|
|
142
|
+
else:
|
|
143
|
+
try:
|
|
144
|
+
values = tuple([int(x) for x in nodes])
|
|
145
|
+
|
|
146
|
+
return values
|
|
147
|
+
except Exception as e:
|
|
148
|
+
return Tuple(_values=nodes)
|
|
149
|
+
|
|
150
|
+
def visit_tree(self, node, visit):
|
|
151
|
+
"""Delegate directly to nested element."""
|
|
152
|
+
return visit[0]
|
|
153
|
+
|
|
154
|
+
def visit_bigraph(self, node, visit):
|
|
155
|
+
"""Alias for tree; allows recursion within nested bigraphs."""
|
|
156
|
+
return visit[0]
|
|
157
|
+
|
|
158
|
+
def visit_group(self, node, visit):
|
|
159
|
+
"""Handle grouped subexpression `( ... )`; return tuple or dict."""
|
|
160
|
+
group_value = visit[1]
|
|
161
|
+
return group_value if isinstance(group_value, (list, tuple, dict, Tuple)) else (group_value,)
|
|
162
|
+
|
|
163
|
+
def visit_nest(self, node, visit):
|
|
164
|
+
"""Handle `key:subtype` pairs (used in trees/maps)."""
|
|
165
|
+
return {visit[0]: visit[2]}
|
|
166
|
+
|
|
167
|
+
def visit_type_name(self, node, visit):
|
|
168
|
+
"""Resolve base type, parameters, and defaults into schema nodes."""
|
|
169
|
+
schema = visit[0]
|
|
170
|
+
|
|
171
|
+
# Parse parameter list
|
|
172
|
+
type_parameters = [
|
|
173
|
+
parameter
|
|
174
|
+
for parameter in visit[1]['visit']]
|
|
175
|
+
|
|
176
|
+
if type_parameters:
|
|
177
|
+
schema = handle_parameters(self.operation, schema, type_parameters[0])
|
|
178
|
+
|
|
179
|
+
# Parse default value `{...}`
|
|
180
|
+
default_visit = visit[2]['visit']
|
|
181
|
+
if default_visit:
|
|
182
|
+
default = default_visit[0]
|
|
183
|
+
if isinstance(schema, Node):
|
|
184
|
+
schema._default = default
|
|
185
|
+
elif isinstance(schema, dict):
|
|
186
|
+
schema['_default'] = default
|
|
187
|
+
|
|
188
|
+
return schema
|
|
189
|
+
|
|
190
|
+
def visit_parameter_list(self, node, visit):
|
|
191
|
+
"""Return ordered list of parameters `[A,B,C]`."""
|
|
192
|
+
first = [visit[1]]
|
|
193
|
+
rest = [inner['visit'][1] for inner in visit[2]['visit']]
|
|
194
|
+
return first + rest
|
|
195
|
+
|
|
196
|
+
def visit_default_block(self, node, visit):
|
|
197
|
+
"""Extract contents of `{...}` blocks."""
|
|
198
|
+
return visit[1]
|
|
199
|
+
|
|
200
|
+
def visit_default(self, node, visit):
|
|
201
|
+
"""Return text inside default braces as string."""
|
|
202
|
+
return node.text
|
|
203
|
+
|
|
204
|
+
def visit_symbol(self, node, visit):
|
|
205
|
+
"""Resolve bare symbol names via the operation registry or parse visitor."""
|
|
206
|
+
return self.operation.access(node.text)
|
|
207
|
+
|
|
208
|
+
def visit_nothing(self, node, visit):
|
|
209
|
+
"""Handle empty productions (e.g., trailing commas)."""
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
def generic_visit(self, node, visit):
|
|
213
|
+
"""Fallback: return raw parse node and visited children."""
|
|
214
|
+
return {'node': node, 'visit': visit}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class Core:
|
|
218
|
+
"""Bigraph-schema operation: registry, parsing, normalization, and ops.
|
|
219
|
+
|
|
220
|
+
- Maintains a registry mapping type keys to node constructors (see `BASE_TYPES`).
|
|
221
|
+
- Normalizes schema representations (strings, dicts, lists) into dataclass nodes
|
|
222
|
+
via `access(...)` using the bigraph grammar (`parse.visit_expression`).
|
|
223
|
+
- Exposes core methods (`infer`, `render`, `default`, `resolve`, `check`,
|
|
224
|
+
`serialize`, `realize`, `merge`, `jump`, `traverse`, `bind`, `apply`).
|
|
225
|
+
- Post-access invariants: e.g., `Array._shape -> tuple[int,...]`,
|
|
226
|
+
`Array._data -> numpy.dtype`; node fields like `_values`, `_options`,
|
|
227
|
+
`_key/_value`, `_inputs/_outputs` are populated.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def __init__(self, types):
|
|
231
|
+
"""Initialize operation with a base type registry (e.g., `BASE_TYPES`)."""
|
|
232
|
+
|
|
233
|
+
self.packages_distributions = {
|
|
234
|
+
key: list(set(values))
|
|
235
|
+
for key, values in importlib.metadata.packages_distributions().items()
|
|
236
|
+
for value in values}
|
|
237
|
+
|
|
238
|
+
self.distributions_packages = {
|
|
239
|
+
value: key
|
|
240
|
+
for key, values in self.packages_distributions.items()
|
|
241
|
+
for value in values}
|
|
242
|
+
|
|
243
|
+
self.registry = {}
|
|
244
|
+
self.link_registry = {}
|
|
245
|
+
self.method_registry = {}
|
|
246
|
+
|
|
247
|
+
self.parse_visitor = CoreVisitor(self)
|
|
248
|
+
|
|
249
|
+
self.register_types(types)
|
|
250
|
+
self.register_link('edge', Edge)
|
|
251
|
+
|
|
252
|
+
def register_type(self, key, data):
|
|
253
|
+
"""Register a single type key; deep-merge if it already exists."""
|
|
254
|
+
found = self.access(data)
|
|
255
|
+
if key in self.registry:
|
|
256
|
+
self.update_type(key, found)
|
|
257
|
+
else:
|
|
258
|
+
self.registry[key] = found
|
|
259
|
+
|
|
260
|
+
def register_types(self, types):
|
|
261
|
+
"""Bulk register multiple type keys into the operation registry."""
|
|
262
|
+
for key, data in types.items():
|
|
263
|
+
self.register_type(key, data)
|
|
264
|
+
|
|
265
|
+
def update_type(self, key, data):
|
|
266
|
+
"""Deep-merge metadata/overrides into an existing registry entry."""
|
|
267
|
+
if self.registry[key] != data:
|
|
268
|
+
self.registry[key] = self.resolve(self.registry[key], data)
|
|
269
|
+
|
|
270
|
+
def register_link(self, key, link):
|
|
271
|
+
if key in self.registry:
|
|
272
|
+
self.update_link(key, link)
|
|
273
|
+
else:
|
|
274
|
+
self.link_registry[key] = link
|
|
275
|
+
|
|
276
|
+
def register_links(self, links):
|
|
277
|
+
"""Bulk register multiple link into the operation registry."""
|
|
278
|
+
for key, link in links.items():
|
|
279
|
+
self.register_link(key, link)
|
|
280
|
+
|
|
281
|
+
def update_link(self, key, data):
|
|
282
|
+
"""Deep-merge metadata/overrides into an existing registry entry."""
|
|
283
|
+
self.link_registry[key] = data
|
|
284
|
+
|
|
285
|
+
def register_method(self, key, data):
|
|
286
|
+
self.method_registry[key] = data
|
|
287
|
+
|
|
288
|
+
def call_method(self, key, *args, **kwargs):
|
|
289
|
+
method = self.method_registry.get(key)
|
|
290
|
+
if method is None:
|
|
291
|
+
raise Exception(f'no method {key} in the method registry')
|
|
292
|
+
|
|
293
|
+
return method(self, *args, **kwargs)
|
|
294
|
+
|
|
295
|
+
def select_fields(self, base, schema):
|
|
296
|
+
"""Project dict `schema` onto dataclass `base` fields, normalizing values via `access` (except `_default`)."""
|
|
297
|
+
select = {}
|
|
298
|
+
for key in base.__dataclass_fields__.keys():
|
|
299
|
+
schema_key = schema.get(key)
|
|
300
|
+
if schema_key:
|
|
301
|
+
down = schema_key if key == '_default' else self.access(schema_key)
|
|
302
|
+
select[key] = down
|
|
303
|
+
return select
|
|
304
|
+
|
|
305
|
+
def make_instance(self, base, state):
|
|
306
|
+
"""Instantiate dataclass `base` from dict `state` after field selection/normalization."""
|
|
307
|
+
fields = self.select_fields(base, state)
|
|
308
|
+
instance = base(**fields)
|
|
309
|
+
return instance
|
|
310
|
+
|
|
311
|
+
def access_type(self, value):
|
|
312
|
+
if isinstance(value, dict) and '_type' in value:
|
|
313
|
+
schema = self.access(value['_type'])
|
|
314
|
+
|
|
315
|
+
default_value = None
|
|
316
|
+
if '_default' in value:
|
|
317
|
+
default_value = value['_default']
|
|
318
|
+
elif isinstance(schema, Node) and schema._default is not None:
|
|
319
|
+
default_value = schema._default
|
|
320
|
+
|
|
321
|
+
if not isinstance(schema, Node):
|
|
322
|
+
raise Exception(f'accessing {value} but schema is not found\n{schema}')
|
|
323
|
+
|
|
324
|
+
schema = replace(
|
|
325
|
+
schema,
|
|
326
|
+
**{'_default': default_value})
|
|
327
|
+
|
|
328
|
+
parameters = {
|
|
329
|
+
key: subvalue
|
|
330
|
+
for key, subvalue in value.items()
|
|
331
|
+
if not key in ('_type', '_default')}
|
|
332
|
+
|
|
333
|
+
schema = reify_schema(self, schema, parameters)
|
|
334
|
+
return schema
|
|
335
|
+
|
|
336
|
+
def resolve_inherit(self, key):
|
|
337
|
+
result = {}
|
|
338
|
+
if '_inherit' in key:
|
|
339
|
+
inherit = key['_inherit']
|
|
340
|
+
if not isinstance(inherit, list):
|
|
341
|
+
inherit = [inherit]
|
|
342
|
+
for ancestor in inherit:
|
|
343
|
+
found = self.access(ancestor)
|
|
344
|
+
if not result:
|
|
345
|
+
result = found
|
|
346
|
+
else:
|
|
347
|
+
result = self.resolve(result, found)
|
|
348
|
+
return result
|
|
349
|
+
|
|
350
|
+
def access(self, key):
|
|
351
|
+
"""Interpret an encoded schema or object and produce a compiled node.
|
|
352
|
+
|
|
353
|
+
Converts strings, dicts, or lists into dataclass-based schema instances.
|
|
354
|
+
Acts as the main entry point for parsing bigraph expressions and building
|
|
355
|
+
normalized in-memory representations.
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
# TODO: consider other terms for this?
|
|
359
|
+
# * compile
|
|
360
|
+
# * parse
|
|
361
|
+
|
|
362
|
+
if is_dataclass(key):
|
|
363
|
+
return key
|
|
364
|
+
|
|
365
|
+
elif isinstance(key, str):
|
|
366
|
+
if key not in self.registry:
|
|
367
|
+
try:
|
|
368
|
+
return visit_expression(key, self.parse_visitor)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
raise Exception(f'unable to parse type "{key}"\n\ndue to\n{e}')
|
|
371
|
+
else:
|
|
372
|
+
entry = self.registry[key]
|
|
373
|
+
if callable(entry):
|
|
374
|
+
return entry()
|
|
375
|
+
elif isinstance(entry, Node):
|
|
376
|
+
return entry
|
|
377
|
+
elif isinstance(entry, dict):
|
|
378
|
+
return self.access(entry)
|
|
379
|
+
|
|
380
|
+
elif isinstance(key, dict):
|
|
381
|
+
if '_type' in key:
|
|
382
|
+
return self.access_type(key)
|
|
383
|
+
|
|
384
|
+
else:
|
|
385
|
+
result = self.resolve_inherit(key)
|
|
386
|
+
|
|
387
|
+
for subkey, subitem in key.items():
|
|
388
|
+
if isinstance(subkey, str):
|
|
389
|
+
subitem = subitem if subkey.startswith('_') else self.access(subitem)
|
|
390
|
+
if isinstance(result, Node):
|
|
391
|
+
if hasattr(result, subkey):
|
|
392
|
+
result = replace(result, **{subkey: subitem})
|
|
393
|
+
else:
|
|
394
|
+
setattr(result, subkey, subitem)
|
|
395
|
+
else:
|
|
396
|
+
result[subkey] = subitem
|
|
397
|
+
|
|
398
|
+
return result
|
|
399
|
+
|
|
400
|
+
elif isinstance(key, list):
|
|
401
|
+
return [self.access(element) for element in key]
|
|
402
|
+
else:
|
|
403
|
+
return key
|
|
404
|
+
|
|
405
|
+
def infer_merges(self, state, path=()):
|
|
406
|
+
"""Derive a schema that matches the structure of an example state.
|
|
407
|
+
|
|
408
|
+
Analyzes values to infer types, shapes, and nested relationships, generating
|
|
409
|
+
a schema node that captures the structure of the provided data.
|
|
410
|
+
"""
|
|
411
|
+
return infer(self, state, path=path)
|
|
412
|
+
|
|
413
|
+
def infer(self, state, path=()):
|
|
414
|
+
"""Derive a schema that matches the structure of an example state.
|
|
415
|
+
|
|
416
|
+
Analyzes values to infer types, shapes, and nested relationships, generating
|
|
417
|
+
a schema node that captures the structure of the provided data.
|
|
418
|
+
"""
|
|
419
|
+
schema, merges = infer(self, state, path=path)
|
|
420
|
+
merge_schema = self.resolve_merges(schema, merges)
|
|
421
|
+
|
|
422
|
+
return merge_schema
|
|
423
|
+
|
|
424
|
+
def render(self, schema, defaults=False):
|
|
425
|
+
"""Produce a serializable view of a compiled schema.
|
|
426
|
+
|
|
427
|
+
Converts internal dataclass nodes into JSON-friendly dicts or strings.
|
|
428
|
+
This is the inverse of `access()`, ensuring round-trip fidelity between
|
|
429
|
+
code representations and stored schema definitions.
|
|
430
|
+
"""
|
|
431
|
+
found = self.access(schema)
|
|
432
|
+
return render(found, defaults=defaults)
|
|
433
|
+
|
|
434
|
+
def default_merges(self, schema, path=()):
|
|
435
|
+
"""Generate a representative state that satisfies a schema.
|
|
436
|
+
|
|
437
|
+
Uses type defaults and explicit `_default` values to instantiate an example
|
|
438
|
+
state consistent with the given schema.
|
|
439
|
+
"""
|
|
440
|
+
found = self.access(schema)
|
|
441
|
+
|
|
442
|
+
value = default(found)
|
|
443
|
+
return realize(self, found, value, path=path)
|
|
444
|
+
|
|
445
|
+
def default(self, schema, path=()):
|
|
446
|
+
found = self.access(schema)
|
|
447
|
+
value = default(found)
|
|
448
|
+
return self.realize(found, value, path=path)
|
|
449
|
+
|
|
450
|
+
def resolve(self, current_schema, update_schema, path=None):
|
|
451
|
+
"""Unify two schemas under node semantics (e.g., Map/Tree/Link field-wise resolution)."""
|
|
452
|
+
current = self.access(current_schema)
|
|
453
|
+
update = self.access(update_schema)
|
|
454
|
+
|
|
455
|
+
if path:
|
|
456
|
+
return resolve(current, update, path=path)
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
if current == update:
|
|
460
|
+
return current
|
|
461
|
+
else:
|
|
462
|
+
return resolve(current, update)
|
|
463
|
+
except ValueError:
|
|
464
|
+
# numpy grumble grumble
|
|
465
|
+
return resolve(current, update)
|
|
466
|
+
|
|
467
|
+
def generalize(self, current_schema, update_schema):
|
|
468
|
+
"""Unify two schemas under node semantics (e.g., Map/Tree/Link field-wise resolution)."""
|
|
469
|
+
current = self.access(current_schema)
|
|
470
|
+
update = self.access(update_schema)
|
|
471
|
+
return generalize(current, update)
|
|
472
|
+
|
|
473
|
+
def check(self, schema, state):
|
|
474
|
+
"""Returns True if the `state` fits the `schema`."""
|
|
475
|
+
found = self.access(schema)
|
|
476
|
+
return check(found, state)
|
|
477
|
+
|
|
478
|
+
def validate(self, schema, state, message=None):
|
|
479
|
+
"""Returns a nested description of how the state does not match the schema"""
|
|
480
|
+
found = self.access(schema)
|
|
481
|
+
validation = validate(self, found, state)
|
|
482
|
+
if validation:
|
|
483
|
+
message = f'state failed schema validation:\\nschema: {pf(render(schema))}\n\nstate: {pf(state)}'
|
|
484
|
+
raise Exception(f'{message}: {validation}')
|
|
485
|
+
|
|
486
|
+
def serialize(self, schema, state):
|
|
487
|
+
"""Convert a structured Python state into an encoded representation.
|
|
488
|
+
|
|
489
|
+
Encodes typed values into JSON-compatible primitives while respecting the
|
|
490
|
+
schema’s structure and constraints.
|
|
491
|
+
"""
|
|
492
|
+
found = self.access(schema)
|
|
493
|
+
return serialize(found, state)
|
|
494
|
+
|
|
495
|
+
def realize(self, schema, state, path=()):
|
|
496
|
+
"""Convert an encoded representation back into structured Python values.
|
|
497
|
+
|
|
498
|
+
Decodes strings, numbers, and nested structures into their appropriate types,
|
|
499
|
+
guided by the provided schema.
|
|
500
|
+
"""
|
|
501
|
+
found = self.access(schema)
|
|
502
|
+
|
|
503
|
+
decode_schema, decode_state, merges = realize(
|
|
504
|
+
self,
|
|
505
|
+
found,
|
|
506
|
+
state,
|
|
507
|
+
path=path)
|
|
508
|
+
|
|
509
|
+
if merges:
|
|
510
|
+
merge_schema = self.resolve_merges({}, merges)
|
|
511
|
+
decode_schema = self.resolve(decode_schema, merge_schema)
|
|
512
|
+
merge_state = self.fill(merge_schema, decode_state)
|
|
513
|
+
else:
|
|
514
|
+
merge_state = decode_state
|
|
515
|
+
|
|
516
|
+
return decode_schema, merge_state
|
|
517
|
+
|
|
518
|
+
def generalize_merges(self, schema, merges):
|
|
519
|
+
if len(merges) > 0:
|
|
520
|
+
merge_schema = {}
|
|
521
|
+
for path, subschema in merges:
|
|
522
|
+
merge_schema = self.resolve(
|
|
523
|
+
merge_schema,
|
|
524
|
+
subschema,
|
|
525
|
+
resolve_path(path))
|
|
526
|
+
|
|
527
|
+
schema = self.generalize(schema, merge_schema)
|
|
528
|
+
|
|
529
|
+
return schema
|
|
530
|
+
|
|
531
|
+
def resolve_merges(self, schema, merges):
|
|
532
|
+
if len(merges) > 0:
|
|
533
|
+
merge_schema = {}
|
|
534
|
+
for path, subschema in merges:
|
|
535
|
+
merge_schema = self.resolve(
|
|
536
|
+
merge_schema,
|
|
537
|
+
subschema,
|
|
538
|
+
resolve_path(path))
|
|
539
|
+
|
|
540
|
+
schema = self.resolve(schema, merge_schema)
|
|
541
|
+
|
|
542
|
+
return schema
|
|
543
|
+
|
|
544
|
+
def resolve_schemas(self, schemas):
|
|
545
|
+
if len(schemas) > 0:
|
|
546
|
+
schema = schemas[0]
|
|
547
|
+
for subschema in schemas[1:]:
|
|
548
|
+
schema = self.resolve(schema, subschema)
|
|
549
|
+
return schema
|
|
550
|
+
|
|
551
|
+
def jump(self, schema, state, raw_key):
|
|
552
|
+
"""Navigate by logical jump (`Key`/`Index`/`Star`)."""
|
|
553
|
+
found = self.access(schema)
|
|
554
|
+
key = convert_jump(raw_key)
|
|
555
|
+
context = blank_context(found, state, ())
|
|
556
|
+
return jump(found, state, key, context)
|
|
557
|
+
|
|
558
|
+
def traverse(self, schema, state, raw_path):
|
|
559
|
+
"""Traverse along a resolved path (supports `..` and wildcards) via `convert_path`."""
|
|
560
|
+
found = self.access(schema)
|
|
561
|
+
path = convert_path(raw_path)
|
|
562
|
+
context = blank_context(found, state, path)
|
|
563
|
+
return traverse(found, state, path, context)
|
|
564
|
+
|
|
565
|
+
def bind(self, schema, state, raw_key, target):
|
|
566
|
+
"""Bind a logical key (jump) to a target."""
|
|
567
|
+
found = self.access(schema)
|
|
568
|
+
key = convert_jump(raw_key)
|
|
569
|
+
return bind(found, state, key, target)
|
|
570
|
+
|
|
571
|
+
def merge(self, schema, state, merge_state, path=()):
|
|
572
|
+
"""Schema-aware merge of `merge_state` into `state`."""
|
|
573
|
+
found = self.access(schema)
|
|
574
|
+
return merge(found, state, merge_state, path=path)
|
|
575
|
+
|
|
576
|
+
def fill(self, schema, state, overwrite=False):
|
|
577
|
+
found = self.access(schema)
|
|
578
|
+
base_schema, base_state, merges = self.default_merges(found)
|
|
579
|
+
merge_schema = self.resolve_merges(base_schema, merges)
|
|
580
|
+
|
|
581
|
+
if overwrite:
|
|
582
|
+
return merge(merge_schema, state, base_state)
|
|
583
|
+
else:
|
|
584
|
+
return merge(merge_schema, base_state, state)
|
|
585
|
+
|
|
586
|
+
def view_ports(self, schema, state, path, ports_schema, wires):
|
|
587
|
+
if isinstance(wires, str):
|
|
588
|
+
wires = [wires]
|
|
589
|
+
|
|
590
|
+
if isinstance(wires, (list, tuple)):
|
|
591
|
+
_, result = self.traverse(schema, state, list(path) + list(wires))
|
|
592
|
+
|
|
593
|
+
elif isinstance(wires, dict):
|
|
594
|
+
result = {}
|
|
595
|
+
for port_key, subport in wires.items():
|
|
596
|
+
subschema, subwires = self.jump(
|
|
597
|
+
ports_schema,
|
|
598
|
+
wires,
|
|
599
|
+
port_key)
|
|
600
|
+
|
|
601
|
+
inner_view = self.view_ports(
|
|
602
|
+
schema,
|
|
603
|
+
state,
|
|
604
|
+
path,
|
|
605
|
+
subschema,
|
|
606
|
+
subwires)
|
|
607
|
+
|
|
608
|
+
if inner_view is not None:
|
|
609
|
+
result[port_key] = inner_view
|
|
610
|
+
|
|
611
|
+
else:
|
|
612
|
+
raise Exception(f'trying to view state at path {path} with these ports:\n{ports_schema}\nbut not sure what these wires are:\n{wires}')
|
|
613
|
+
|
|
614
|
+
return result
|
|
615
|
+
|
|
616
|
+
def view(self, schema, state, link_path, ports_key='inputs'):
|
|
617
|
+
found = self.access(schema)
|
|
618
|
+
link_schema, link_state = self.traverse(schema, state, link_path)
|
|
619
|
+
ports_schema = getattr(link_schema, f'_{ports_key}')
|
|
620
|
+
wires = link_state.get(ports_key) or {}
|
|
621
|
+
view = self.view_ports(
|
|
622
|
+
schema,
|
|
623
|
+
state,
|
|
624
|
+
link_path[:-1],
|
|
625
|
+
ports_schema,
|
|
626
|
+
wires)
|
|
627
|
+
|
|
628
|
+
return view
|
|
629
|
+
|
|
630
|
+
def project_ports(self, ports_schema, wires, path, view):
|
|
631
|
+
project_schema = {}
|
|
632
|
+
project_state = {}
|
|
633
|
+
|
|
634
|
+
if isinstance(wires, str):
|
|
635
|
+
wires = [wires]
|
|
636
|
+
|
|
637
|
+
if isinstance(wires, (list, tuple)):
|
|
638
|
+
destination = resolve_path(list(path) + list(wires))
|
|
639
|
+
|
|
640
|
+
project_schema = self.resolve(
|
|
641
|
+
project_schema,
|
|
642
|
+
ports_schema,
|
|
643
|
+
path=destination)
|
|
644
|
+
|
|
645
|
+
project_state = self.merge(
|
|
646
|
+
project_schema,
|
|
647
|
+
project_state,
|
|
648
|
+
view,
|
|
649
|
+
path=destination)
|
|
650
|
+
|
|
651
|
+
elif isinstance(wires, dict):
|
|
652
|
+
if isinstance(view, list):
|
|
653
|
+
result = [
|
|
654
|
+
self.project_ports(ports_schema, wires, path, state)
|
|
655
|
+
for state in view]
|
|
656
|
+
project_schema = Tuple(_values=[
|
|
657
|
+
item[0]
|
|
658
|
+
for item in result])
|
|
659
|
+
project_state = [
|
|
660
|
+
item[1]
|
|
661
|
+
for item in result]
|
|
662
|
+
else:
|
|
663
|
+
branches = []
|
|
664
|
+
for key, subwires in wires.items():
|
|
665
|
+
subports, subview = self.jump(ports_schema, view, key)
|
|
666
|
+
if subview is None:
|
|
667
|
+
continue
|
|
668
|
+
|
|
669
|
+
subschema, substate = self.project_ports(
|
|
670
|
+
subports,
|
|
671
|
+
subwires,
|
|
672
|
+
path,
|
|
673
|
+
subview)
|
|
674
|
+
|
|
675
|
+
if substate is not None:
|
|
676
|
+
branches.append((subschema, substate))
|
|
677
|
+
|
|
678
|
+
project_schema = Node()
|
|
679
|
+
project_state = {}
|
|
680
|
+
for branch_schema, branch_state in branches:
|
|
681
|
+
project_schema = resolve(project_schema, branch_schema)
|
|
682
|
+
project_state = self.merge(project_schema, project_state, branch_state)
|
|
683
|
+
|
|
684
|
+
else:
|
|
685
|
+
raise Exception(
|
|
686
|
+
f'inverting state\n {view}\naccording to ports schema\n {ports_schema}\nbut wires are not recognized\n {wires}')
|
|
687
|
+
|
|
688
|
+
return project_schema, project_state
|
|
689
|
+
|
|
690
|
+
def project(self, schema, state, link_path, view, ports_key='outputs'):
|
|
691
|
+
found = self.access(schema)
|
|
692
|
+
link_schema, link_state = self.traverse(schema, state, link_path)
|
|
693
|
+
ports_schema = getattr(link_schema, f'_{ports_key}')
|
|
694
|
+
wires = link_state.get(ports_key) or {}
|
|
695
|
+
project_schema, project_state = self.project_ports(
|
|
696
|
+
ports_schema,
|
|
697
|
+
wires,
|
|
698
|
+
link_path[:-1],
|
|
699
|
+
view)
|
|
700
|
+
|
|
701
|
+
return project_schema, project_state
|
|
702
|
+
|
|
703
|
+
def combine(self, schema, state, update_schema, update_state):
|
|
704
|
+
resolved = self.resolve(schema, update_schema)
|
|
705
|
+
merged = self.merge(resolved, state, update_state)
|
|
706
|
+
decode_schema, decode_state = self.realize(resolved, merged)
|
|
707
|
+
|
|
708
|
+
return decode_schema, decode_state
|
|
709
|
+
|
|
710
|
+
def link_state(self, link, path):
|
|
711
|
+
result_schema = {}
|
|
712
|
+
result_state = {}
|
|
713
|
+
|
|
714
|
+
instance = link.get('instance')
|
|
715
|
+
|
|
716
|
+
if instance is not None:
|
|
717
|
+
initial_state = instance.initial_state()
|
|
718
|
+
|
|
719
|
+
for ports_key in ['inputs', 'outputs']:
|
|
720
|
+
ports_schema = link.get(f'_{ports_key}', {})
|
|
721
|
+
wires = link.get(ports_key, {})
|
|
722
|
+
project_schema, project_state = self.project_ports(ports_schema, wires, path[:-1], initial_state)
|
|
723
|
+
result_schema, result_state = self.combine(
|
|
724
|
+
result_schema, result_state,
|
|
725
|
+
project_schema, project_state)
|
|
726
|
+
|
|
727
|
+
return result_schema, result_state
|
|
728
|
+
|
|
729
|
+
def wire_schema(self, schema, state, wires, path=None):
|
|
730
|
+
outcome = {}
|
|
731
|
+
path = path or []
|
|
732
|
+
|
|
733
|
+
if isinstance(wires, dict):
|
|
734
|
+
for key, subwires in wires.items():
|
|
735
|
+
outcome[key] = self.wire_schema(
|
|
736
|
+
schema,
|
|
737
|
+
state,
|
|
738
|
+
wires[key],
|
|
739
|
+
path)
|
|
740
|
+
|
|
741
|
+
else:
|
|
742
|
+
outcome, _ = self.traverse(schema, state, path + wires)
|
|
743
|
+
|
|
744
|
+
return outcome
|
|
745
|
+
|
|
746
|
+
def apply(self, schema, state, update, path=()):
|
|
747
|
+
"""Apply a schema-aware update/patch; provides minimal context."""
|
|
748
|
+
if update:
|
|
749
|
+
found = self.access(schema)
|
|
750
|
+
return apply(found, state, update, path)
|
|
751
|
+
else:
|
|
752
|
+
return state, []
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def allocate_core(top=None):
|
|
756
|
+
core = Core(BASE_TYPES)
|
|
757
|
+
core = discover_packages(core, top)
|
|
758
|
+
|
|
759
|
+
return core
|