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.
@@ -0,0 +1,249 @@
1
+ from pprint import pformat as pf
2
+ from plum import dispatch
3
+ import numpy as np
4
+
5
+ from bigraph_schema.schema import (
6
+ Node,
7
+ Atom,
8
+ Empty,
9
+ Union,
10
+ Tuple,
11
+ Boolean,
12
+ Number,
13
+ Integer,
14
+ Float,
15
+ Delta,
16
+ Nonnegative,
17
+ String,
18
+ Enum,
19
+ Wrap,
20
+ Maybe,
21
+ Overwrite,
22
+ List,
23
+ Map,
24
+ Tree,
25
+ Array,
26
+ Key,
27
+ Path,
28
+ Wires,
29
+ Schema,
30
+ Link,
31
+ )
32
+
33
+ from bigraph_schema.methods.check import check
34
+ from bigraph_schema.methods.serialize import render
35
+
36
+
37
+ @dispatch
38
+ def validate(core, schema: Empty, state):
39
+ if state is not None:
40
+ return f'Empty schema is not empty:\n\n{pf(state)}\n\n'
41
+
42
+
43
+ @dispatch
44
+ def validate(core, schema: Maybe, state):
45
+ if state is not None:
46
+ return validate(core, schema._value, state)
47
+
48
+
49
+ @dispatch
50
+ def validate(core, schema: Wrap, state):
51
+ return validate(core, schema._value, state)
52
+
53
+
54
+ @dispatch
55
+ def validate(core, schema: Union, state):
56
+ for option in schema._options:
57
+ result = validate(core, option, state)
58
+ if not result:
59
+ return result
60
+
61
+ return f'Union values did not match state:\n\n{pf(render(schema))}\n\n{pf(state)}\n\n'
62
+
63
+
64
+ def filter_nones(results):
65
+ return [
66
+ result
67
+ for result in results
68
+ if result is not None]
69
+
70
+ @dispatch
71
+ def validate(core, schema: Tuple, state):
72
+ if not isinstance(state, (list, tuple)):
73
+ return f'Tuple schema requires tuple values:\n\n{pf(render(schema))}\n\nnot:\n\n{pf(state)}\n\n'
74
+
75
+ elif len(schema._values) == len(state):
76
+ results = filter_nones([
77
+ validate(core, subschema, value)
78
+ for subschema, value in zip(schema._values, state)])
79
+
80
+ if results:
81
+ return results
82
+
83
+ else:
84
+ return f'Tuple schema and state are different lengths:\n\n{pf(render(schema))}\n\n{pf(state)}\n\n'
85
+
86
+
87
+ @dispatch
88
+ def validate(core, schema: Boolean, state):
89
+ if not isinstance(state, bool):
90
+ return f'Boolean schema but state is not boolean:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
91
+
92
+
93
+ @dispatch
94
+ def validate(core, schema: Integer, state):
95
+ if not isinstance(state, int):
96
+ return f'Integer schema but state is not an integer:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
97
+
98
+
99
+ @dispatch
100
+ def validate(core, schema: Float, state):
101
+ if not isinstance(state, float):
102
+ return f'Float schema but state is not a float:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
103
+
104
+
105
+ @dispatch
106
+ def validate(core, schema: Nonnegative, state):
107
+ if state < 0:
108
+ return f'Nonnegative schema but state is negative:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
109
+
110
+
111
+ @dispatch
112
+ def validate(core, schema: String, state):
113
+ if not isinstance(state, str):
114
+ return f'Float schema but state is not a float:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
115
+
116
+
117
+ @dispatch
118
+ def validate(core, schema: Enum, state):
119
+ if not isinstance(state, str):
120
+ return f'Enum schema but state is not a string:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
121
+
122
+ if not state in schema._values:
123
+ return f'Enum schema but state is not in the enumeration:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
124
+
125
+
126
+ @dispatch
127
+ def validate(core, schema: List, state):
128
+ if not isinstance(state, (list, tuple)):
129
+ return f'List schema but state is not a list:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
130
+
131
+ results = filter_nones([
132
+ validate(core, schema._element, element)
133
+ for element in state])
134
+
135
+ if results:
136
+ return results
137
+
138
+ @dispatch
139
+ def validate(core, schema: Map, state):
140
+ if not isinstance(state, dict):
141
+ return f'Map schema but state is not a map:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
142
+
143
+ results = filter_nones([
144
+ validate(core, schema._value, value)
145
+ for value in state.values()])
146
+
147
+ if results:
148
+ return results
149
+
150
+ elif not isinstance(schema._key, String):
151
+ results = filter_nones([
152
+ validate(
153
+ core,
154
+ schema._key,
155
+ realize(
156
+ core,
157
+ schema._key,
158
+ key))
159
+ for key in state.keys()])
160
+
161
+ if results:
162
+ return results
163
+
164
+
165
+ @dispatch
166
+ def validate(core, schema: Tree, state):
167
+ leaf_validate = validate(core, schema._leaf, state)
168
+
169
+ if leaf_validate:
170
+ if isinstance(state, dict):
171
+ results = filter_nones([
172
+ validate(core, schema, branch)
173
+ for key, branch in state.items()])
174
+ if results:
175
+ return f'Tree schema but state matches neither leaf nor tree:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
176
+
177
+ else:
178
+ return f'Tree schema but state matches neither leaf nor tree:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
179
+
180
+
181
+ @dispatch
182
+ def validate(core, schema: Array, state):
183
+ if not isinstance(state, np.ndarray):
184
+ return f'Array schema but state is not an array:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
185
+
186
+ shape_match = tuple(schema._shape) == state.shape
187
+ data_match = schema._data == state.dtype
188
+
189
+ if not shape_match:
190
+ return f'Array schema but shape does not match:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
191
+ if not data_match:
192
+ return f'Array schema but data does not match:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
193
+
194
+
195
+ @dispatch
196
+ def validate(core, schema: Key, state):
197
+ if not isinstance(state, int) or isinstance(state, str):
198
+ return f'Key schema but state is not a key:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
199
+
200
+
201
+ @dispatch
202
+ def validate(core, schema: Node, state):
203
+ fields = [
204
+ field
205
+ for field in schema.__dataclass_fields__
206
+ if not field.startswith('_')]
207
+
208
+ if fields:
209
+ if isinstance(state, dict):
210
+ result = {}
211
+ for key in schema.__dataclass_fields__:
212
+ if not key.startswith('_'):
213
+ if key not in state:
214
+ return f'Node schema but key "{key}" is not in state:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
215
+ else:
216
+ down = validate(
217
+ core,
218
+ getattr(schema, key),
219
+ state[key])
220
+
221
+ if down:
222
+ result[key] = down
223
+ if result:
224
+ return result
225
+ else:
226
+ subcheck = check(schema, state)
227
+ if not subcheck:
228
+ return f'Node schema but state does not match:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
229
+
230
+
231
+ @dispatch
232
+ def validate(core, schema: dict, state):
233
+ result = {}
234
+ for key, subschema in schema.items():
235
+ if key not in state:
236
+ continue
237
+ # result[key] = f'Schema has key "{key}" but state does not:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
238
+ else:
239
+ subresult = validate(core, subschema, state[key])
240
+ if subresult:
241
+ result[key] = subresult
242
+
243
+ if result:
244
+ return result
245
+
246
+
247
+ @dispatch
248
+ def validate(core, schema, state):
249
+ return f'Schema and state are not known:\n\nschema: {pf(render(schema))}\n\nstate: {pf(state)}\n\n'
@@ -0,0 +1 @@
1
+ from bigraph_schema.package.discover import discover_packages
@@ -0,0 +1,122 @@
1
+ import importlib
2
+ import importlib.metadata
3
+ import pkgutil
4
+ import inspect
5
+ from typing import Dict, List, Tuple, Set, Type
6
+
7
+ from bigraph_schema import Edge
8
+
9
+
10
+ def find_edges(mapping, module_name=None):
11
+ discovered = []
12
+ for _, cls in mapping:
13
+ # Only classes defined in this module (not imported into it)
14
+ if not inspect.isclass(cls):
15
+ continue
16
+
17
+ if module_name and cls.__module__ != module_name:
18
+ continue
19
+
20
+ if not issubclass(cls, Edge) or cls is Edge:
21
+ continue
22
+
23
+ # Use the true module path for a stable registration key
24
+ fq_name = f"{cls.__module__}.{cls.__name__}"
25
+ discovered.append((fq_name, cls))
26
+
27
+ return discovered
28
+
29
+
30
+ def recursive_dynamic_import(
31
+ core,
32
+ module,
33
+ visited: Set[str] | None = None,
34
+ ) -> tuple[object, List[tuple[str, Type[Edge]]]]:
35
+ if visited is None:
36
+ visited = set()
37
+
38
+ if inspect.ismodule(module):
39
+ adjusted = module.__name__
40
+ if adjusted in visited:
41
+ return core, [], visited
42
+
43
+ visited.add(adjusted)
44
+
45
+ if isinstance(module, str):
46
+ adjusted = core.distributions_packages.get(module, module)
47
+ if adjusted in visited:
48
+ return core, [], visited
49
+ visited.add(adjusted)
50
+
51
+ try:
52
+ module = importlib.import_module(adjusted)
53
+
54
+ except ModuleNotFoundError as e:
55
+ # e.name is the missing module name
56
+ # If the missing name IS the module we tried to import, then it's truly not found.
57
+ # Otherwise, it's a dependency import failure inside that module.
58
+ print(f"module `{adjusted}` not found during dynamic import")
59
+
60
+ # Allow module to register types into core
61
+ if hasattr(module, "register_types"):
62
+ core = module.register_types(core)
63
+
64
+ mapping = inspect.getmembers(module, inspect.isclass)
65
+ discovered = find_edges(mapping)
66
+
67
+ # Recurse into submodules if this is a package
68
+ if hasattr(module, "__path__"):
69
+ for _, subname, _ in pkgutil.iter_modules(module.__path__):
70
+ submod = f"{adjusted}.{subname}"
71
+ core, sub_discovered, visited = recursive_dynamic_import(
72
+ core, submod, visited=visited)
73
+ discovered.extend(sub_discovered)
74
+
75
+ return core, discovered, visited
76
+
77
+
78
+ def is_process_library(dist: importlib.metadata.Distribution) -> bool:
79
+ if dist.metadata["Name"] == "bigraph-schema":
80
+ return True
81
+ reqs = dist.requires or []
82
+ return any("bigraph-schema" in r for r in reqs)
83
+
84
+
85
+ def load_local_modules(core, top=None) -> tuple[object, List[tuple[str, Type[Edge]]]]:
86
+ processes = []
87
+ visited = set([])
88
+
89
+ for dist_name in core.distributions_packages:
90
+ dist = importlib.metadata.distribution(dist_name)
91
+ if not is_process_library(dist):
92
+ continue
93
+
94
+ core, found, visited = recursive_dynamic_import(
95
+ core,
96
+ dist_name,
97
+ visited=visited)
98
+
99
+ processes.extend(found)
100
+
101
+ if top:
102
+ for key, value in top.items():
103
+ if inspect.isclass(value) and issubclass(value, Edge):
104
+ processes.append((key, value))
105
+
106
+ if key == 'register_types':
107
+ core = value(core)
108
+
109
+ return core, processes
110
+
111
+
112
+ def discover_packages(core, top=None):
113
+ core, discovered = load_local_modules(core, top=top)
114
+
115
+ for fq_name, edge_cls in discovered:
116
+ core.register_link(fq_name, edge_cls)
117
+
118
+ short = fq_name.split(".")[-1]
119
+ if short not in core.link_registry:
120
+ core.register_link(short, edge_cls)
121
+
122
+ return core
@@ -0,0 +1,183 @@
1
+ """
2
+ ======================
3
+ Parse Bigraph Notation
4
+ ======================
5
+
6
+ Parses and renders bigraph-style type expressions used in schema definitions,
7
+ such as parameterized types, nested trees, merges (`|`), and unions (`~`).
8
+ """
9
+
10
+ from parsimonious.grammar import Grammar
11
+ from parsimonious.nodes import NodeVisitor
12
+
13
+ # --- Example Expressions -----------------------------------------------------
14
+ parameter_examples = {
15
+ 'no-parameters': 'simple',
16
+ 'one-parameter': 'parameterized[A]',
17
+ 'three-parameters': 'parameterized[A,B,C]',
18
+ 'nested-parameters': 'nested[outer[inner]]',
19
+ 'multiple-nested-parameters': 'nested[outer[inner],other,later[on,there[is],more]]',
20
+ 'bars': 'a|b|c|zzzz',
21
+ 'union': 'a~b~c~zzzz',
22
+ 'typed': 'a:field[yellow,tree,snake]|b:(x:earth|y:cloud|z:sky)',
23
+ 'typed_parameters': 'edge[a:int|b:(x:length|y:float),v[zz:float|xx:what]]',
24
+ 'inputs_and_outputs': 'edge[input1:float|input2:int,output1:float|output2:int]',
25
+ 'tuple': 'what[is,happening|(with:yellow|this:green)|this:now]',
26
+ 'single': 'hello[(3),over]',
27
+ 'double': 'hello[(3|4),over{owiehf;a832hf9237fh!@#(&$A(HFO@I#}]',
28
+ 'double_default': 'hello[(3|4),over]{3,3,3,3,3}',
29
+ 'units_type': 'length^2*mass/time^1_5',
30
+ 'nothing': '()'}
31
+
32
+ # --- Grammar -----------------------------------------------------------------
33
+ parameter_grammar = Grammar(
34
+ """
35
+ expression = merge / union / tree / nothing
36
+ merge = tree (bar tree)+
37
+ union = tree (tilde tree)+
38
+ tree = bigraph / type_name
39
+ bigraph = group / nest
40
+ group = paren_left expression paren_right
41
+ nest = symbol colon tree
42
+ type_name = symbol parameter_list? default_block?
43
+ parameter_list = square_left expression (comma expression)* square_right
44
+ default_block = curly_left default curly_right
45
+ default = ~r"[^}]*"
46
+ symbol = ~r"[\\w\\d-_/<>*&^%$#@!`+ ]+"
47
+ dot = "."
48
+ colon = ":"
49
+ bar = "|"
50
+ paren_left = "("
51
+ paren_right = ")"
52
+ square_left = "["
53
+ square_right = "]"
54
+ curly_left = "{"
55
+ curly_right = "}"
56
+ comma = ","
57
+ tilde = "~"
58
+ not_newline = ~r"[^\\n\\r]"*
59
+ newline = ~"[\\n\\r]+"
60
+ ws = ~r"\\s*"
61
+ nothing = ""
62
+ """)
63
+
64
+
65
+ # --- Visitor -----------------------------------------------------------------
66
+ class ParameterVisitor(NodeVisitor):
67
+ """Visitor that walks a parsed tree and builds structured type expressions."""
68
+
69
+ def visit_expression(self, node, visit):
70
+ return visit[0]
71
+
72
+ def visit_union(self, node, visit):
73
+ head = [visit[0]]
74
+ tail = [tree['visit'][1] for tree in visit[1]['visit']]
75
+ return {'_union': head + tail}
76
+
77
+ def visit_merge(self, node, visit):
78
+ head = [visit[0]]
79
+ tail = [tree['visit'][1] for tree in visit[1]['visit']]
80
+ nodes = head + tail
81
+
82
+ if all(isinstance(tree, dict) for tree in nodes):
83
+ merged = {}
84
+ for tree in nodes:
85
+ merged.update(tree)
86
+ return merged
87
+ else:
88
+ return tuple(nodes)
89
+
90
+ def visit_tree(self, node, visit):
91
+ return visit[0]
92
+
93
+ def visit_bigraph(self, node, visit):
94
+ return visit[0]
95
+
96
+ def visit_group(self, node, visit):
97
+ group_value = visit[1]
98
+ return group_value if isinstance(group_value, (list, tuple, dict)) else (group_value,)
99
+
100
+ def visit_nest(self, node, visit):
101
+ return {visit[0]: visit[2]}
102
+
103
+ def visit_type_name(self, node, visit):
104
+ type_name = visit[0]
105
+ type_parameters = visit[1]['visit']
106
+ if type_parameters:
107
+ return [type_name, type_parameters[0]]
108
+ return type_name
109
+
110
+ def visit_parameter_list(self, node, visit):
111
+ first = [visit[1]]
112
+ rest = [inner['visit'][1] for inner in visit[2]['visit']]
113
+ return first + rest
114
+
115
+ def visit_symbol(self, node, visit):
116
+ return node.text
117
+
118
+ def visit_nothing(self, node, visit):
119
+ return {}
120
+
121
+ def generic_visit(self, node, visit):
122
+ return {'node': node, 'visit': visit}
123
+
124
+ # --- API ---------------------------------------------------------------------
125
+ def parsed_leftmost_leaf(parsed):
126
+ if len(parsed.children) == 0:
127
+ return parsed
128
+ else:
129
+ return parsed_leftmost_leaf(parsed.children[0])
130
+
131
+ def visit_expression(expression, visitor):
132
+ parsed = parameter_grammar.parse(expression)
133
+ leaf = parsed_leftmost_leaf(parsed)
134
+ if hasattr(leaf, 'match') and leaf.match[0] == expression:
135
+ return expression
136
+ else:
137
+ return visitor.visit(parsed)
138
+
139
+ def parse_expression(expression):
140
+ """
141
+ Parse a bigraph-style type expression into a structured Python object.
142
+ """
143
+ visitor = ParameterVisitor()
144
+ return visit_expression(expression, visitor)
145
+
146
+ def is_type_expression(expression):
147
+ """
148
+ Return True if the expression is a parameterized type list.
149
+ """
150
+ return isinstance(expression, list) and len(expression) == 2 and isinstance(expression[1], list)
151
+
152
+ def render_expression(expression):
153
+ """
154
+ Render a structured type expression back into a bigraph-style string.
155
+ """
156
+ if isinstance(expression, str):
157
+ return expression
158
+ elif isinstance(expression, list):
159
+ type_name, parameters = expression
160
+ inner = ','.join(render_expression(p) for p in parameters)
161
+ return f'{type_name}[{inner}]'
162
+ elif isinstance(expression, tuple):
163
+ return '(' + '|'.join(render_expression(e) for e in expression) + ')'
164
+ elif isinstance(expression, dict):
165
+ if '_union' in expression:
166
+ return '~'.join(render_expression(p) for p in expression['_union'])
167
+ else:
168
+ parts = [f'{k}:{render_expression(v)}' for k, v in expression.items()]
169
+ return f'({ "|".join(parts) })'
170
+ return str(expression) # fallback for unknown structures
171
+
172
+ # --- Debugging/Testing ---------------------------------------------------------
173
+ def test_parse_parameters():
174
+ """Test parsing and rendering for all example expressions."""
175
+ for label, expr in parameter_examples.items():
176
+ parsed = parse_expression(expr)
177
+ print(f'{label}: {expr}')
178
+ if parsed:
179
+ print(f' Parsed: {parsed}')
180
+ print(f' Rendered: {render_expression(parsed)}')
181
+
182
+ if __name__ == '__main__':
183
+ test_parse_parameters()
@@ -0,0 +1,57 @@
1
+ """
2
+ =========
3
+ Protocols
4
+ =========
5
+
6
+ This module contains the protocols for retrieving processes from address.
7
+ """
8
+
9
+ import sys
10
+ import inspect
11
+ import importlib
12
+
13
+
14
+ def function_module(function):
15
+ """
16
+ Retrieves the fully qualified name of a given function.
17
+ """
18
+ module = inspect.getmodule(function)
19
+
20
+ return f'{module.__name__}.{function.__name__}'
21
+
22
+
23
+ def local_lookup_module(address):
24
+ """Local Module Protocol
25
+
26
+ Retrieves local module
27
+ """
28
+ if '.' in address:
29
+ module_name, class_name = address.rsplit('.', 1)
30
+ module = importlib.import_module(module_name)
31
+ return getattr(module, class_name)
32
+ else:
33
+ module = sys.modules[__name__]
34
+ if hasattr(module, address):
35
+ return getattr(sys.modules[__name__], address)
36
+
37
+
38
+ def local_lookup_registry(core, address):
39
+ """Process Registry Protocol
40
+
41
+ Retrieves from the process registry
42
+ """
43
+ return core.link_registry.get(address)
44
+
45
+
46
+ def local_lookup(core, address):
47
+ """Local Lookup Protocol
48
+
49
+ Retrieves local processes, from the process registry or from a local module
50
+ """
51
+ if address[0] == '!':
52
+ instantiate = local_lookup_module(address[1:])
53
+ else:
54
+ instantiate = local_lookup_registry(core, address)
55
+ return instantiate
56
+
57
+