oatdb 0.4.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.
- oatdb/__init__.py +1111 -0
- oatdb-0.4.0.dist-info/METADATA +364 -0
- oatdb-0.4.0.dist-info/RECORD +4 -0
- oatdb-0.4.0.dist-info/WHEEL +4 -0
oatdb/__init__.py
ADDED
|
@@ -0,0 +1,1111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OatDB Python Client - Improved Version
|
|
3
|
+
|
|
4
|
+
This client provides a fluent API for building and executing OatDB queries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import List, Dict, Any, Optional, Union
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from hashlib import sha256
|
|
14
|
+
from pldag import PLDAG, CompilationSetting
|
|
15
|
+
from graphlib import TopologicalSorter
|
|
16
|
+
from abc import ABC, abstractmethod
|
|
17
|
+
from itertools import chain
|
|
18
|
+
from more_itertools import unique_everseen
|
|
19
|
+
|
|
20
|
+
def sub_to_pldag(sub_data: Dict[str, dict]) -> PLDAG:
|
|
21
|
+
"""Convert OatDB sub response to PLDAG."""
|
|
22
|
+
dag = PLDAG(compilation_setting=CompilationSetting.ON_DEMAND)
|
|
23
|
+
structured = {}
|
|
24
|
+
|
|
25
|
+
for node_id, node_data in sub_data.items():
|
|
26
|
+
k, v = list(node_data.items())[0]
|
|
27
|
+
if k == "Primitive":
|
|
28
|
+
structured[node_id] = {
|
|
29
|
+
"type": "primitive",
|
|
30
|
+
"bound": complex(v[0], v[1])
|
|
31
|
+
}
|
|
32
|
+
elif k == "Composite":
|
|
33
|
+
structured[node_id] = {
|
|
34
|
+
"type": "composite",
|
|
35
|
+
"coefficients": dict(v['coefficients']),
|
|
36
|
+
"bias": v['bias'][0]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Build dependency graph
|
|
40
|
+
dependencies = {k: set() for k in structured.keys()}
|
|
41
|
+
for node_id, node_data in filter(lambda x: x[1]['type'] == "composite", structured.items()):
|
|
42
|
+
for dep in node_data['coefficients'].keys():
|
|
43
|
+
dependencies[node_id].add(dep)
|
|
44
|
+
|
|
45
|
+
# Add nodes in topological order
|
|
46
|
+
id_pointers = {}
|
|
47
|
+
for node_id in TopologicalSorter(dependencies).static_order():
|
|
48
|
+
node_data = structured[node_id]
|
|
49
|
+
if node_data['type'] == "primitive":
|
|
50
|
+
dag_id = dag.set_primitive(id=node_id, bound=node_data['bound'])
|
|
51
|
+
id_pointers[node_id] = dag_id
|
|
52
|
+
elif node_data['type'] == "composite":
|
|
53
|
+
dag_id = dag.set_gelineq(
|
|
54
|
+
coefficients={id_pointers.get(k,k): v for k, v in node_data['coefficients'].items()},
|
|
55
|
+
bias=node_data['bias'],
|
|
56
|
+
alias=node_id
|
|
57
|
+
)
|
|
58
|
+
id_pointers[node_id] = dag_id
|
|
59
|
+
|
|
60
|
+
dag.compile()
|
|
61
|
+
return dag
|
|
62
|
+
|
|
63
|
+
@lru_cache
|
|
64
|
+
def pldag_to_sub(dag: PLDAG) -> Dict[str, dict]:
|
|
65
|
+
"""Convert PLDAG to OatDB sub format."""
|
|
66
|
+
sub_data = {}
|
|
67
|
+
prims = dag.primitives
|
|
68
|
+
comps = dag.composites
|
|
69
|
+
for node_id in map(str, dag._toposort):
|
|
70
|
+
if node_id in prims:
|
|
71
|
+
bound = dag.get(node_id)
|
|
72
|
+
sub_data[node_id] = {"Primitive": [int(bound.real), int(bound.imag)]}
|
|
73
|
+
elif node_id in comps:
|
|
74
|
+
row_idx = dag._row(node_id)
|
|
75
|
+
coefficients = [
|
|
76
|
+
[str(dag.id_to_alias(k) or k), int(v)]
|
|
77
|
+
for k, v in zip(dag.columns, dag._amat[row_idx])
|
|
78
|
+
if v != 0
|
|
79
|
+
]
|
|
80
|
+
b = int(dag._bvec[row_idx].real)
|
|
81
|
+
alias_as_id = dag.id_to_alias(node_id)
|
|
82
|
+
if not alias_as_id:
|
|
83
|
+
raise ValueError(f"Composite node {node_id} missing source ID for sub conversion.")
|
|
84
|
+
|
|
85
|
+
sub_data[alias_as_id] = {
|
|
86
|
+
"Composite": {
|
|
87
|
+
"coefficients": coefficients,
|
|
88
|
+
"bias": [b, b]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return sub_data
|
|
92
|
+
|
|
93
|
+
RefOrId = Union['FunctionCall', str]
|
|
94
|
+
RefOrValue = Union['FunctionCall', str, int, List, Dict]
|
|
95
|
+
RefOrBound = Union['FunctionCall', complex]
|
|
96
|
+
RefOrPlDAG = Union['FunctionCall', PLDAG]
|
|
97
|
+
|
|
98
|
+
@dataclass(eq=False)
|
|
99
|
+
class FunctionCall(ABC):
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _serialize_for_hash(obj):
|
|
103
|
+
"""Recursively serialize objects for hashing, converting FunctionCalls to their out IDs.
|
|
104
|
+
|
|
105
|
+
IMPORTANT: This must serialize objects the same way they appear in json_output(),
|
|
106
|
+
so that the hash is based on what actually gets sent to the server.
|
|
107
|
+
"""
|
|
108
|
+
if isinstance(obj, FunctionCall):
|
|
109
|
+
return f"<ref:{obj.out}>"
|
|
110
|
+
elif isinstance(obj, dict):
|
|
111
|
+
return {
|
|
112
|
+
FunctionCall._serialize_for_hash(k): FunctionCall._serialize_for_hash(v)
|
|
113
|
+
for k, v in obj.items()
|
|
114
|
+
}
|
|
115
|
+
elif isinstance(obj, (list, tuple)):
|
|
116
|
+
return [FunctionCall._serialize_for_hash(v) for v in obj]
|
|
117
|
+
elif isinstance(obj, complex):
|
|
118
|
+
return [obj.real, obj.imag]
|
|
119
|
+
elif isinstance(obj, set):
|
|
120
|
+
return sorted([FunctionCall._serialize_for_hash(v) for v in obj])
|
|
121
|
+
elif isinstance(obj, (str, int, float, bool)) or obj is None:
|
|
122
|
+
return obj
|
|
123
|
+
elif isinstance(obj, PLDAG):
|
|
124
|
+
# Convert PLDAG to sub format for hashing, matching json_output()
|
|
125
|
+
# This ensures hash is based on what gets sent to server
|
|
126
|
+
return pldag_to_sub(obj)
|
|
127
|
+
else:
|
|
128
|
+
return obj
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _hash_data(d: dict) -> str:
|
|
132
|
+
serialized = FunctionCall._serialize_for_hash(d)
|
|
133
|
+
hash_data = json.dumps(serialized, sort_keys=True)
|
|
134
|
+
return sha256(hash_data.encode()).hexdigest()
|
|
135
|
+
|
|
136
|
+
def _fn(self) -> str:
|
|
137
|
+
return self.__class__.__name__
|
|
138
|
+
|
|
139
|
+
def __hash__(self) -> int:
|
|
140
|
+
# Use full SHA256 hash (64 hex chars = 256 bits)
|
|
141
|
+
# Python can handle arbitrarily large integers
|
|
142
|
+
return int(self.out, 16)
|
|
143
|
+
|
|
144
|
+
def __eq__(self, other) -> bool:
|
|
145
|
+
if not isinstance(other, FunctionCall):
|
|
146
|
+
return False
|
|
147
|
+
return self.out == other.out
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def out(self) -> str:
|
|
151
|
+
# Serialize args recursively, handling nested FunctionCalls
|
|
152
|
+
serialized = self._serialize_for_hash({**self.__dict__, **{"fn": self._fn()}})
|
|
153
|
+
hash_data = json.dumps(serialized, sort_keys=True)
|
|
154
|
+
return sha256(hash_data.encode()).hexdigest()
|
|
155
|
+
|
|
156
|
+
def json_input(self, data: dict) -> Any:
|
|
157
|
+
"""Converts json data coming back from the server into the appropriate format."""
|
|
158
|
+
return data
|
|
159
|
+
|
|
160
|
+
@abstractmethod
|
|
161
|
+
def json_output(self) -> list:
|
|
162
|
+
"""Build the payload list for this call, including dependencies."""
|
|
163
|
+
raise NotImplementedError()
|
|
164
|
+
|
|
165
|
+
def _ref_or_value(self, value: RefOrValue) -> Union[dict, str, int, List]:
|
|
166
|
+
"""Convert a value to either a reference or direct value."""
|
|
167
|
+
if isinstance(value, FunctionCall):
|
|
168
|
+
return {"$ref": value.out}
|
|
169
|
+
return value
|
|
170
|
+
|
|
171
|
+
def _ref_or_dag(self, dag: RefOrPlDAG) -> Union[dict, str]:
|
|
172
|
+
"""Convert a PLDAG to either a reference or direct DAG representation."""
|
|
173
|
+
if issubclass(dag.__class__, FunctionCall):
|
|
174
|
+
return {"$ref": dag.out}
|
|
175
|
+
return pldag_to_sub(dag)
|
|
176
|
+
|
|
177
|
+
def _ref_or_bound(self, bound: RefOrBound) -> Union[dict, List[int]]:
|
|
178
|
+
"""Convert a bound to either a reference or direct bound."""
|
|
179
|
+
if issubclass(bound.__class__, FunctionCall):
|
|
180
|
+
return self._ref_or_value(bound)
|
|
181
|
+
return [int(bound.real), int(bound.imag)]
|
|
182
|
+
|
|
183
|
+
def _ref_or_value_list(self, values: Union["FunctionCall", List[RefOrValue]]) -> List[Union[dict, str]]:
|
|
184
|
+
"""Convert a list of values to references or direct values."""
|
|
185
|
+
if issubclass(values.__class__, FunctionCall):
|
|
186
|
+
return self._ref_or_value(values)
|
|
187
|
+
return [self._ref_or_value(v) for v in values]
|
|
188
|
+
|
|
189
|
+
@dataclass(eq=False)
|
|
190
|
+
class set_property(FunctionCall):
|
|
191
|
+
|
|
192
|
+
id: RefOrValue
|
|
193
|
+
property: RefOrValue
|
|
194
|
+
value: RefOrValue
|
|
195
|
+
|
|
196
|
+
def json_output(self) -> list:
|
|
197
|
+
earlier_builds = []
|
|
198
|
+
if issubclass(self.id.__class__, FunctionCall):
|
|
199
|
+
earlier_builds += self.id.json_output()
|
|
200
|
+
if issubclass(self.property.__class__, FunctionCall):
|
|
201
|
+
earlier_builds += self.property.json_output()
|
|
202
|
+
if issubclass(self.value.__class__, FunctionCall):
|
|
203
|
+
earlier_builds += self.value.json_output()
|
|
204
|
+
|
|
205
|
+
return earlier_builds + [
|
|
206
|
+
{
|
|
207
|
+
"fn": "set_property",
|
|
208
|
+
"args": {
|
|
209
|
+
"id": self._ref_or_value(self.id),
|
|
210
|
+
"property": self._ref_or_value(self.property),
|
|
211
|
+
"value": self._ref_or_value(self.value)
|
|
212
|
+
},
|
|
213
|
+
"out": self.out
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
@dataclass(eq=False)
|
|
218
|
+
class set_primitive(FunctionCall):
|
|
219
|
+
|
|
220
|
+
id: Union[str, FunctionCall]
|
|
221
|
+
bound: Union[complex, FunctionCall] = 1j
|
|
222
|
+
|
|
223
|
+
def json_output(self) -> list:
|
|
224
|
+
earlier_builds = []
|
|
225
|
+
if issubclass(self.id.__class__, FunctionCall):
|
|
226
|
+
earlier_builds += self.id.json_output()
|
|
227
|
+
if issubclass(self.bound.__class__, FunctionCall):
|
|
228
|
+
earlier_builds += self.bound.json_output()
|
|
229
|
+
return earlier_builds + [
|
|
230
|
+
{
|
|
231
|
+
"fn": "set_primitive",
|
|
232
|
+
"args": {
|
|
233
|
+
"id": self._ref_or_value(self.id),
|
|
234
|
+
"bound": self._ref_or_value(self.bound) if issubclass(self.bound.__class__, FunctionCall) else [
|
|
235
|
+
int(self.bound.real),
|
|
236
|
+
int(self.bound.imag)
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
"out": self.out
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
@dataclass(eq=False)
|
|
244
|
+
class set_primitives(FunctionCall):
|
|
245
|
+
|
|
246
|
+
ids: Union[List[RefOrValue], FunctionCall]
|
|
247
|
+
bound: Union[complex, FunctionCall] = 1j
|
|
248
|
+
|
|
249
|
+
def json_output(self) -> list:
|
|
250
|
+
earlier_builds = []
|
|
251
|
+
if issubclass(self.ids.__class__, FunctionCall):
|
|
252
|
+
earlier_builds += self.ids.json_output()
|
|
253
|
+
else:
|
|
254
|
+
for id in self.ids:
|
|
255
|
+
if issubclass(id.__class__, FunctionCall):
|
|
256
|
+
earlier_builds += id.json_output()
|
|
257
|
+
|
|
258
|
+
if issubclass(self.bound.__class__, FunctionCall):
|
|
259
|
+
earlier_builds += self.bound.json_output()
|
|
260
|
+
|
|
261
|
+
return earlier_builds + [
|
|
262
|
+
{
|
|
263
|
+
"fn": "set_primitives",
|
|
264
|
+
"args": {
|
|
265
|
+
"ids": self._ref_or_value_list(self.ids),
|
|
266
|
+
"bound": self._ref_or_value(self.bound) if issubclass(self.bound.__class__, FunctionCall) else [int(self.bound.real), int(self.bound.imag)]
|
|
267
|
+
},
|
|
268
|
+
"out": self.out
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
@dataclass(eq=False)
|
|
273
|
+
class set_gelineq(FunctionCall):
|
|
274
|
+
|
|
275
|
+
coefficients: Union[Dict[Union[str, FunctionCall], Union[int, FunctionCall]], FunctionCall]
|
|
276
|
+
bias: Union[int, FunctionCall]
|
|
277
|
+
alias: Optional[str] = None
|
|
278
|
+
|
|
279
|
+
def json_output(self) -> list:
|
|
280
|
+
earlier_builds = []
|
|
281
|
+
if issubclass(self.coefficients.__class__, FunctionCall):
|
|
282
|
+
earlier_builds = self.coefficients.json_output()
|
|
283
|
+
else:
|
|
284
|
+
for k in self.coefficients.keys():
|
|
285
|
+
if issubclass(k.__class__, FunctionCall):
|
|
286
|
+
earlier_builds += k.json_output()
|
|
287
|
+
for v in self.coefficients.values():
|
|
288
|
+
if issubclass(v.__class__, FunctionCall):
|
|
289
|
+
earlier_builds += v.json_output()
|
|
290
|
+
|
|
291
|
+
build_bias = self.bias.json_output() if issubclass(self.bias.__class__, FunctionCall) else []
|
|
292
|
+
return earlier_builds + build_bias + [
|
|
293
|
+
{
|
|
294
|
+
"fn": "set_gelineq",
|
|
295
|
+
"args": {
|
|
296
|
+
"coefficients": self._ref_or_value(self.coefficients) if issubclass(self.coefficients.__class__, FunctionCall) else [
|
|
297
|
+
{
|
|
298
|
+
"id": self._ref_or_value(k),
|
|
299
|
+
"coefficient": self._ref_or_value(v)
|
|
300
|
+
}
|
|
301
|
+
for k, v in self.coefficients.items()
|
|
302
|
+
],
|
|
303
|
+
"bias": self._ref_or_value(self.bias),
|
|
304
|
+
},
|
|
305
|
+
"out": self.out
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@dataclass(eq=False)
|
|
311
|
+
class SetThresholdOperatorCall(FunctionCall):
|
|
312
|
+
"""Base class for set_atleast, set_atmost, set_equal."""
|
|
313
|
+
|
|
314
|
+
references: Union[List[RefOrValue], FunctionCall]
|
|
315
|
+
value: Union[int, FunctionCall]
|
|
316
|
+
alias: Optional[str] = None
|
|
317
|
+
|
|
318
|
+
def json_output(self) -> list:
|
|
319
|
+
earlier_builds = []
|
|
320
|
+
if issubclass(self.references.__class__, FunctionCall):
|
|
321
|
+
earlier_builds += self.references.json_output()
|
|
322
|
+
else:
|
|
323
|
+
for ref in self.references:
|
|
324
|
+
if issubclass(ref.__class__, FunctionCall):
|
|
325
|
+
earlier_builds += ref.json_output()
|
|
326
|
+
|
|
327
|
+
if issubclass(self.value.__class__, FunctionCall):
|
|
328
|
+
earlier_builds += self.value.json_output()
|
|
329
|
+
|
|
330
|
+
return earlier_builds + [
|
|
331
|
+
{
|
|
332
|
+
"fn": self._fn(),
|
|
333
|
+
"args": {
|
|
334
|
+
"references": self._ref_or_value_list(self.references),
|
|
335
|
+
"value": self._ref_or_value(self.value),
|
|
336
|
+
**({"alias": self.alias} if self.alias is not None else {})
|
|
337
|
+
},
|
|
338
|
+
"out": self.out
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
@dataclass(eq=False)
|
|
343
|
+
class SetLogicalOperatorCall(FunctionCall):
|
|
344
|
+
"""Base class for set_and, set_or, set_not, set_xor."""
|
|
345
|
+
|
|
346
|
+
references: Union[List[RefOrValue], FunctionCall]
|
|
347
|
+
alias: Optional[str] = None
|
|
348
|
+
|
|
349
|
+
def json_output(self) -> list:
|
|
350
|
+
earlier_builds = []
|
|
351
|
+
if issubclass(self.references.__class__, FunctionCall):
|
|
352
|
+
earlier_builds += self.references.json_output()
|
|
353
|
+
else:
|
|
354
|
+
for ref in self.references:
|
|
355
|
+
if issubclass(ref.__class__, FunctionCall):
|
|
356
|
+
earlier_builds += ref.json_output()
|
|
357
|
+
|
|
358
|
+
return earlier_builds + [
|
|
359
|
+
{
|
|
360
|
+
"fn": self._fn(),
|
|
361
|
+
"args": {
|
|
362
|
+
"references": self._ref_or_value_list(self.references),
|
|
363
|
+
**({"alias": self.alias} if self.alias is not None else {})
|
|
364
|
+
},
|
|
365
|
+
"out": self.out
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
|
|
369
|
+
@dataclass(eq=False)
|
|
370
|
+
class SetBinaryOperatorCall(FunctionCall):
|
|
371
|
+
"""Base class for set_imply, set_equiv."""
|
|
372
|
+
|
|
373
|
+
lhs: RefOrValue
|
|
374
|
+
rhs: RefOrValue
|
|
375
|
+
alias: Optional[str] = None
|
|
376
|
+
|
|
377
|
+
def json_output(self) -> list:
|
|
378
|
+
earlier_builds = []
|
|
379
|
+
if issubclass(self.lhs.__class__, FunctionCall):
|
|
380
|
+
earlier_builds += self.lhs.json_output()
|
|
381
|
+
if issubclass(self.rhs.__class__, FunctionCall):
|
|
382
|
+
earlier_builds += self.rhs.json_output()
|
|
383
|
+
|
|
384
|
+
return earlier_builds + [
|
|
385
|
+
{
|
|
386
|
+
"fn": self._fn(),
|
|
387
|
+
"args": {
|
|
388
|
+
"lhs": self._ref_or_value(self.lhs),
|
|
389
|
+
"rhs": self._ref_or_value(self.rhs),
|
|
390
|
+
**({"alias": self.alias} if self.alias is not None else {})
|
|
391
|
+
},
|
|
392
|
+
"out": self.out
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
@dataclass(eq=False)
|
|
397
|
+
class set_atleast(SetThresholdOperatorCall):
|
|
398
|
+
pass
|
|
399
|
+
|
|
400
|
+
@dataclass(eq=False)
|
|
401
|
+
class set_atmost(SetThresholdOperatorCall):
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
@dataclass(eq=False)
|
|
405
|
+
class set_equal(SetThresholdOperatorCall):
|
|
406
|
+
pass
|
|
407
|
+
|
|
408
|
+
@dataclass(eq=False)
|
|
409
|
+
class set_and(SetLogicalOperatorCall):
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
@dataclass(eq=False)
|
|
413
|
+
class set_or(SetLogicalOperatorCall):
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
@dataclass(eq=False)
|
|
417
|
+
class set_not(SetLogicalOperatorCall):
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
@dataclass(eq=False)
|
|
421
|
+
class set_xor(SetLogicalOperatorCall):
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
@dataclass(eq=False)
|
|
425
|
+
class set_imply(SetBinaryOperatorCall):
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
@dataclass(eq=False)
|
|
429
|
+
class set_equiv(SetBinaryOperatorCall):
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
@dataclass(eq=False)
|
|
433
|
+
class sub(FunctionCall):
|
|
434
|
+
"""Call to sub function."""
|
|
435
|
+
|
|
436
|
+
root: RefOrPlDAG
|
|
437
|
+
|
|
438
|
+
def json_input(self, data: dict) -> PLDAG:
|
|
439
|
+
return sub_to_pldag(data)
|
|
440
|
+
|
|
441
|
+
def json_output(self) -> list:
|
|
442
|
+
build_root = self.root.json_output() if issubclass(self.root.__class__, FunctionCall) else []
|
|
443
|
+
return build_root + [
|
|
444
|
+
{
|
|
445
|
+
"fn": "sub",
|
|
446
|
+
"args": {
|
|
447
|
+
"root": self._ref_or_value(self.root)
|
|
448
|
+
},
|
|
449
|
+
"out": self.out
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
@dataclass(eq=False)
|
|
454
|
+
class get_node_ids(FunctionCall):
|
|
455
|
+
|
|
456
|
+
filter: Optional[RefOrValue] = None
|
|
457
|
+
|
|
458
|
+
def json_output(self) -> list:
|
|
459
|
+
build_filter = self.filter.json_output() if issubclass(self.filter.__class__, FunctionCall) else []
|
|
460
|
+
return build_filter + [
|
|
461
|
+
{
|
|
462
|
+
"fn": self._fn(),
|
|
463
|
+
"args": {
|
|
464
|
+
**({"filter": self._ref_or_value(self.filter)} if self.filter is not None else {})
|
|
465
|
+
},
|
|
466
|
+
"out": self.out
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
@dataclass(eq=False)
|
|
471
|
+
class get_nodes(FunctionCall):
|
|
472
|
+
|
|
473
|
+
ids: Union[List[RefOrValue], FunctionCall]
|
|
474
|
+
|
|
475
|
+
def json_output(self) -> list:
|
|
476
|
+
build_ids = self.ids.json_output() if issubclass(self.ids.__class__, FunctionCall) else []
|
|
477
|
+
return build_ids + [
|
|
478
|
+
{
|
|
479
|
+
"fn": self._fn(),
|
|
480
|
+
"args": {
|
|
481
|
+
"ids": self._ref_or_value_list(self.ids)
|
|
482
|
+
},
|
|
483
|
+
"out": self.out
|
|
484
|
+
}
|
|
485
|
+
]
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@dataclass(eq=False)
|
|
489
|
+
class get_node(FunctionCall):
|
|
490
|
+
|
|
491
|
+
id: RefOrValue
|
|
492
|
+
|
|
493
|
+
def json_output(self) -> list:
|
|
494
|
+
build_id = self.id.json_output() if issubclass(self.id.__class__, FunctionCall) else []
|
|
495
|
+
return build_id + [
|
|
496
|
+
{
|
|
497
|
+
"fn": self._fn(),
|
|
498
|
+
"args": {
|
|
499
|
+
"id": self._ref_or_value(self.id)
|
|
500
|
+
},
|
|
501
|
+
"out": self.out
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@dataclass(eq=False)
|
|
507
|
+
class get_property_values(FunctionCall):
|
|
508
|
+
|
|
509
|
+
property: RefOrValue
|
|
510
|
+
|
|
511
|
+
def json_output(self) -> list:
|
|
512
|
+
return [
|
|
513
|
+
{
|
|
514
|
+
"fn": self._fn(),
|
|
515
|
+
"args": {
|
|
516
|
+
"property": self._ref_or_value(self.property)
|
|
517
|
+
},
|
|
518
|
+
"out": self.out
|
|
519
|
+
}
|
|
520
|
+
]
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@dataclass(eq=False)
|
|
524
|
+
class get_alias(FunctionCall):
|
|
525
|
+
|
|
526
|
+
id: RefOrValue
|
|
527
|
+
|
|
528
|
+
def json_output(self) -> list:
|
|
529
|
+
build_id = self.id.json_output() if issubclass(self.id.__class__, FunctionCall) else []
|
|
530
|
+
return build_id + [
|
|
531
|
+
{
|
|
532
|
+
"fn": self._fn(),
|
|
533
|
+
"args": {
|
|
534
|
+
"id": self._ref_or_value(self.id)
|
|
535
|
+
},
|
|
536
|
+
"out": self.out
|
|
537
|
+
}
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
@dataclass(eq=False)
|
|
542
|
+
class get_id_from_alias(FunctionCall):
|
|
543
|
+
|
|
544
|
+
alias: RefOrValue
|
|
545
|
+
|
|
546
|
+
def json_output(self) -> list:
|
|
547
|
+
build_alias = self.alias.json_output() if issubclass(self.alias.__class__, FunctionCall) else []
|
|
548
|
+
return build_alias + [
|
|
549
|
+
{
|
|
550
|
+
"fn": self._fn(),
|
|
551
|
+
"args": {
|
|
552
|
+
"alias": self._ref_or_value(self.alias)
|
|
553
|
+
},
|
|
554
|
+
"out": self.out
|
|
555
|
+
}
|
|
556
|
+
]
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@dataclass(eq=False)
|
|
560
|
+
class get_aliases_from_id(FunctionCall):
|
|
561
|
+
|
|
562
|
+
id: RefOrValue
|
|
563
|
+
|
|
564
|
+
def json_output(self) -> list:
|
|
565
|
+
build_id = self.id.json_output() if issubclass(self.id.__class__, FunctionCall) else []
|
|
566
|
+
return build_id + [
|
|
567
|
+
{
|
|
568
|
+
"fn": self._fn(),
|
|
569
|
+
"args": {
|
|
570
|
+
"id": self._ref_or_value(self.id)
|
|
571
|
+
},
|
|
572
|
+
"out": self.out
|
|
573
|
+
}
|
|
574
|
+
]
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
@dataclass(eq=False)
|
|
578
|
+
class get_ids_from_aliases(FunctionCall):
|
|
579
|
+
|
|
580
|
+
aliases: Union[List[RefOrValue], FunctionCall]
|
|
581
|
+
|
|
582
|
+
def json_output(self) -> list:
|
|
583
|
+
build_aliases = self.aliases.json_output() if issubclass(self.aliases.__class__, FunctionCall) else []
|
|
584
|
+
return build_aliases + [
|
|
585
|
+
{
|
|
586
|
+
"fn": self._fn(),
|
|
587
|
+
"args": {
|
|
588
|
+
"aliases": self._ref_or_value_list(self.aliases)
|
|
589
|
+
},
|
|
590
|
+
"out": self.out
|
|
591
|
+
}
|
|
592
|
+
]
|
|
593
|
+
|
|
594
|
+
@dataclass(eq=False)
|
|
595
|
+
class get_ids_from_dag(FunctionCall):
|
|
596
|
+
|
|
597
|
+
dag: RefOrPlDAG
|
|
598
|
+
|
|
599
|
+
def json_output(self) -> list:
|
|
600
|
+
build_dag = self.dag.json_output() if issubclass(self.dag.__class__, FunctionCall) else []
|
|
601
|
+
return build_dag + [
|
|
602
|
+
{
|
|
603
|
+
"fn": self._fn(),
|
|
604
|
+
"args": {
|
|
605
|
+
"dag": self._ref_or_dag(self.dag)
|
|
606
|
+
},
|
|
607
|
+
"out": self.out
|
|
608
|
+
}
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@dataclass(eq=False)
|
|
613
|
+
class propagate(FunctionCall):
|
|
614
|
+
|
|
615
|
+
assignments: Union[Dict[RefOrValue, Union[complex, FunctionCall]], FunctionCall]
|
|
616
|
+
|
|
617
|
+
@property
|
|
618
|
+
def out(self) -> str:
|
|
619
|
+
# Serialize args recursively, handling nested FunctionCalls
|
|
620
|
+
serialized = self._serialize_for_hash({"assignments": self.assignments})
|
|
621
|
+
hash_data = json.dumps(serialized, sort_keys=True)
|
|
622
|
+
return sha256(hash_data.encode()).hexdigest()
|
|
623
|
+
|
|
624
|
+
def json_input(self, data: dict) -> dict:
|
|
625
|
+
return {
|
|
626
|
+
key: complex(value[0], value[1]) for key, value in data.items()
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
def json_output(self) -> list:
|
|
630
|
+
earlier_builds = []
|
|
631
|
+
if issubclass(self.assignments.__class__, FunctionCall):
|
|
632
|
+
earlier_builds += self.assignments.json_output()
|
|
633
|
+
else:
|
|
634
|
+
for k, v in self.assignments.items():
|
|
635
|
+
if issubclass(k.__class__, FunctionCall):
|
|
636
|
+
earlier_builds += k.json_output()
|
|
637
|
+
if issubclass(v.__class__, FunctionCall):
|
|
638
|
+
earlier_builds += v.json_output()
|
|
639
|
+
|
|
640
|
+
return earlier_builds + [
|
|
641
|
+
{
|
|
642
|
+
"fn": "propagate",
|
|
643
|
+
"args": {
|
|
644
|
+
"assignments": self._ref_or_value(self.assignments) if issubclass(self.assignments.__class__, FunctionCall) else [
|
|
645
|
+
{
|
|
646
|
+
"id": self._ref_or_value(k),
|
|
647
|
+
"bound": self._ref_or_value(v) if issubclass(v.__class__, FunctionCall) else [int(v.real), int(v.imag)]
|
|
648
|
+
}
|
|
649
|
+
for k, v in self.assignments.items()
|
|
650
|
+
]
|
|
651
|
+
},
|
|
652
|
+
"out": self.out
|
|
653
|
+
}
|
|
654
|
+
]
|
|
655
|
+
|
|
656
|
+
@dataclass(eq=False)
|
|
657
|
+
class propagate_many(FunctionCall):
|
|
658
|
+
|
|
659
|
+
many_assignments: Union[List[Dict[RefOrValue, RefOrBound]], FunctionCall]
|
|
660
|
+
|
|
661
|
+
def json_input(self, data: dict) -> list:
|
|
662
|
+
return [
|
|
663
|
+
{
|
|
664
|
+
key: complex(value[0], value[1]) for key, value in assignments.items()
|
|
665
|
+
}
|
|
666
|
+
for assignments in data
|
|
667
|
+
]
|
|
668
|
+
|
|
669
|
+
def json_output(self) -> list:
|
|
670
|
+
|
|
671
|
+
earlier_builds = []
|
|
672
|
+
if issubclass(self.many_assignments.__class__, FunctionCall):
|
|
673
|
+
earlier_builds += self.many_assignments.json_output()
|
|
674
|
+
else:
|
|
675
|
+
for assignments in self.many_assignments:
|
|
676
|
+
for assign_id, assign_bound in assignments.items():
|
|
677
|
+
if issubclass(assign_id.__class__, FunctionCall):
|
|
678
|
+
earlier_builds += assign_id.json_output()
|
|
679
|
+
|
|
680
|
+
if issubclass(assign_bound.__class__, FunctionCall):
|
|
681
|
+
earlier_builds += assign_bound.json_output()
|
|
682
|
+
|
|
683
|
+
return earlier_builds + [
|
|
684
|
+
{
|
|
685
|
+
"fn": self._fn(),
|
|
686
|
+
"args": {
|
|
687
|
+
"many_assignments": self._ref_or_value(self.many_assignments) if issubclass(self.many_assignments.__class__, FunctionCall) else [
|
|
688
|
+
[
|
|
689
|
+
{
|
|
690
|
+
"id": self._ref_or_value(assign_id),
|
|
691
|
+
"bound": self._ref_or_value(assign_bound) if issubclass(assign_bound.__class__, FunctionCall) else [
|
|
692
|
+
int(assign_bound.real),
|
|
693
|
+
int(assign_bound.imag)
|
|
694
|
+
]
|
|
695
|
+
}
|
|
696
|
+
for assign_id, assign_bound in assignments.items()
|
|
697
|
+
]
|
|
698
|
+
for assignments in self.many_assignments
|
|
699
|
+
]
|
|
700
|
+
},
|
|
701
|
+
"out": self.out
|
|
702
|
+
}
|
|
703
|
+
]
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
@dataclass(eq=False)
|
|
707
|
+
class sub_many(FunctionCall):
|
|
708
|
+
|
|
709
|
+
roots: Union[List[RefOrValue], FunctionCall]
|
|
710
|
+
|
|
711
|
+
def json_output(self) -> list:
|
|
712
|
+
earlier_builds = []
|
|
713
|
+
if issubclass(self.roots.__class__, FunctionCall):
|
|
714
|
+
earlier_builds += self.roots.json_output()
|
|
715
|
+
else:
|
|
716
|
+
for root in self.roots:
|
|
717
|
+
if issubclass(root.__class__, FunctionCall):
|
|
718
|
+
earlier_builds += root.json_output()
|
|
719
|
+
return earlier_builds + [
|
|
720
|
+
{
|
|
721
|
+
"fn": self._fn(),
|
|
722
|
+
"args": {
|
|
723
|
+
"roots": self._ref_or_value_list(self.roots)
|
|
724
|
+
},
|
|
725
|
+
"out": self.out
|
|
726
|
+
}
|
|
727
|
+
]
|
|
728
|
+
|
|
729
|
+
@dataclass(eq=False)
|
|
730
|
+
class validate(FunctionCall):
|
|
731
|
+
|
|
732
|
+
dag: RefOrPlDAG
|
|
733
|
+
|
|
734
|
+
def json_output(self) -> list:
|
|
735
|
+
build_dag = self.dag.json_output() if issubclass(self.dag.__class__, FunctionCall) else []
|
|
736
|
+
return build_dag + [
|
|
737
|
+
{
|
|
738
|
+
"fn": self._fn(),
|
|
739
|
+
"args": {
|
|
740
|
+
"dag": self._ref_or_dag(self.dag)
|
|
741
|
+
},
|
|
742
|
+
"out": self.out
|
|
743
|
+
}
|
|
744
|
+
]
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
@dataclass(eq=False)
|
|
748
|
+
class ranks(FunctionCall):
|
|
749
|
+
|
|
750
|
+
dag: RefOrPlDAG
|
|
751
|
+
|
|
752
|
+
def json_output(self) -> list:
|
|
753
|
+
build_dag = self.dag.json_output() if issubclass(self.dag.__class__, FunctionCall) else []
|
|
754
|
+
return build_dag + [
|
|
755
|
+
{
|
|
756
|
+
"fn": self._fn(),
|
|
757
|
+
"args": {
|
|
758
|
+
"dag": self._ref_or_dag(self.dag)
|
|
759
|
+
},
|
|
760
|
+
"out": self.out
|
|
761
|
+
}
|
|
762
|
+
]
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
@dataclass(eq=False)
|
|
766
|
+
class delete_node(FunctionCall):
|
|
767
|
+
|
|
768
|
+
id: RefOrValue
|
|
769
|
+
|
|
770
|
+
def json_output(self) -> list:
|
|
771
|
+
build_id = self.id.json_output() if issubclass(self.id.__class__, FunctionCall) else []
|
|
772
|
+
return build_id + [
|
|
773
|
+
{
|
|
774
|
+
"fn": self._fn(),
|
|
775
|
+
"args": {
|
|
776
|
+
"id": self._ref_or_value(self.id)
|
|
777
|
+
},
|
|
778
|
+
"out": self.out
|
|
779
|
+
}
|
|
780
|
+
]
|
|
781
|
+
|
|
782
|
+
@dataclass(eq=False)
|
|
783
|
+
class delete_sub(FunctionCall):
|
|
784
|
+
|
|
785
|
+
roots: Union[List[RefOrValue], FunctionCall]
|
|
786
|
+
|
|
787
|
+
def json_output(self) -> list:
|
|
788
|
+
earlier_builds = []
|
|
789
|
+
if issubclass(self.roots.__class__, FunctionCall):
|
|
790
|
+
earlier_builds += self.roots.json_output()
|
|
791
|
+
else:
|
|
792
|
+
for root in self.roots:
|
|
793
|
+
if issubclass(root.__class__, FunctionCall):
|
|
794
|
+
earlier_builds += root.json_output()
|
|
795
|
+
|
|
796
|
+
return earlier_builds + [
|
|
797
|
+
{
|
|
798
|
+
"fn": self._fn(),
|
|
799
|
+
"args": {
|
|
800
|
+
"roots": self._ref_or_value_list(self.roots) if not issubclass(self.roots.__class__, FunctionCall) else [
|
|
801
|
+
self._ref_or_value(root) for root in self.roots
|
|
802
|
+
]
|
|
803
|
+
},
|
|
804
|
+
"out": self.out
|
|
805
|
+
}
|
|
806
|
+
]
|
|
807
|
+
|
|
808
|
+
@dataclass(eq=False)
|
|
809
|
+
class solve(FunctionCall):
|
|
810
|
+
"""Call to solve function."""
|
|
811
|
+
|
|
812
|
+
dag: RefOrPlDAG
|
|
813
|
+
objective: Union[Dict[RefOrId, RefOrValue], FunctionCall]
|
|
814
|
+
assume: Union[Dict[RefOrId, RefOrBound], FunctionCall]
|
|
815
|
+
maximize: bool = True
|
|
816
|
+
|
|
817
|
+
def json_input(self, inp: Dict[str, List[int]]) -> dict:
|
|
818
|
+
return {
|
|
819
|
+
k: complex(v[0], v[1]) for k, v in inp.items()
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
def json_output(self) -> list:
|
|
823
|
+
earlier_builds = []
|
|
824
|
+
|
|
825
|
+
if issubclass(self.dag.__class__, FunctionCall):
|
|
826
|
+
earlier_builds += self.dag.json_output()
|
|
827
|
+
|
|
828
|
+
if issubclass(self.objective.__class__, FunctionCall):
|
|
829
|
+
earlier_builds += self.objective.json_output()
|
|
830
|
+
|
|
831
|
+
for coef_id, coef_value in self.objective.items():
|
|
832
|
+
if issubclass(coef_id.__class__, FunctionCall):
|
|
833
|
+
earlier_builds += coef_id.json_output()
|
|
834
|
+
|
|
835
|
+
if issubclass(coef_value.__class__, FunctionCall):
|
|
836
|
+
earlier_builds += coef_value.json_output()
|
|
837
|
+
|
|
838
|
+
if issubclass(self.assume.__class__, FunctionCall):
|
|
839
|
+
earlier_builds += self.assume.json_output()
|
|
840
|
+
|
|
841
|
+
for assum_id, assum_bound in self.assume.items():
|
|
842
|
+
if issubclass(assum_id.__class__, FunctionCall):
|
|
843
|
+
earlier_builds += assum_id.json_output()
|
|
844
|
+
|
|
845
|
+
if issubclass(assum_bound.__class__, FunctionCall):
|
|
846
|
+
earlier_builds += assum_bound.json_output()
|
|
847
|
+
|
|
848
|
+
return earlier_builds + [
|
|
849
|
+
{
|
|
850
|
+
"fn": self._fn(),
|
|
851
|
+
"args": {
|
|
852
|
+
"dag": self._ref_or_dag(self.dag),
|
|
853
|
+
"objective": [
|
|
854
|
+
{
|
|
855
|
+
"id": self._ref_or_value(coef_id),
|
|
856
|
+
"coefficient": self._ref_or_value(coef_value)
|
|
857
|
+
}
|
|
858
|
+
for coef_id, coef_value in self.objective.items()
|
|
859
|
+
],
|
|
860
|
+
"assume": [
|
|
861
|
+
{
|
|
862
|
+
"id": self._ref_or_value(assum_id),
|
|
863
|
+
"bound": self._ref_or_value(assum_bound) if issubclass(assum_bound.__class__, FunctionCall) else [
|
|
864
|
+
int(assum_bound.real),
|
|
865
|
+
int(assum_bound.imag)
|
|
866
|
+
]
|
|
867
|
+
}
|
|
868
|
+
for assum_id, assum_bound in self.assume.items()
|
|
869
|
+
],
|
|
870
|
+
"maximize": self.maximize
|
|
871
|
+
},
|
|
872
|
+
"out": self.out
|
|
873
|
+
}
|
|
874
|
+
]
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
@dataclass(eq=False)
|
|
878
|
+
class solve_many(FunctionCall):
|
|
879
|
+
|
|
880
|
+
dag: RefOrPlDAG
|
|
881
|
+
objectives: Union[List[List[Dict[RefOrId, RefOrValue]]], FunctionCall]
|
|
882
|
+
assume: Union[Dict[RefOrId, RefOrBound], FunctionCall]
|
|
883
|
+
maximize: bool = True
|
|
884
|
+
|
|
885
|
+
def json_input(self, data: dict) -> List[Dict[str, complex]]:
|
|
886
|
+
return [
|
|
887
|
+
{
|
|
888
|
+
k: complex(v[0], v[1]) for k, v in assignment.items()
|
|
889
|
+
}
|
|
890
|
+
for assignment in data
|
|
891
|
+
]
|
|
892
|
+
|
|
893
|
+
def json_output(self) -> list:
|
|
894
|
+
earlier_builds = []
|
|
895
|
+
if isinstance(self.dag, FunctionCall):
|
|
896
|
+
earlier_builds += self.dag.json_output()
|
|
897
|
+
|
|
898
|
+
if isinstance(self.objectives, FunctionCall):
|
|
899
|
+
earlier_builds += self.objectives.json_output()
|
|
900
|
+
|
|
901
|
+
for objective in self.objectives:
|
|
902
|
+
if isinstance(objective, FunctionCall):
|
|
903
|
+
earlier_builds += objective.json_output()
|
|
904
|
+
|
|
905
|
+
for obj_id, obj_coef in objective.items():
|
|
906
|
+
if isinstance(obj_id, FunctionCall):
|
|
907
|
+
earlier_builds += obj_id.json_output()
|
|
908
|
+
|
|
909
|
+
if isinstance(obj_coef, FunctionCall):
|
|
910
|
+
earlier_builds += obj_coef.json_output()
|
|
911
|
+
|
|
912
|
+
if issubclass(self.assume.__class__, FunctionCall):
|
|
913
|
+
earlier_builds += self.assume.json_output()
|
|
914
|
+
|
|
915
|
+
for ass_id, ass_bound in self.assume.items():
|
|
916
|
+
if issubclass(ass_id.__class__, FunctionCall):
|
|
917
|
+
earlier_builds += ass_id.json_output()
|
|
918
|
+
|
|
919
|
+
if issubclass(ass_bound.__class__, FunctionCall):
|
|
920
|
+
earlier_builds += ass_bound.json_output()
|
|
921
|
+
|
|
922
|
+
return earlier_builds + [{
|
|
923
|
+
"fn": self._fn(),
|
|
924
|
+
"args": {
|
|
925
|
+
"dag": self._ref_or_dag(self.dag),
|
|
926
|
+
"objectives": self._ref_or_value(self.objectives) if issubclass(self.objectives.__class__, FunctionCall) else [
|
|
927
|
+
[
|
|
928
|
+
{
|
|
929
|
+
"id": self._ref_or_value(obj_id),
|
|
930
|
+
"coefficient": self._ref_or_value(obj_val)
|
|
931
|
+
}
|
|
932
|
+
for obj_id, obj_val in objective.items()
|
|
933
|
+
]
|
|
934
|
+
for objective in self.objectives
|
|
935
|
+
],
|
|
936
|
+
"assume": self._ref_or_value(self.assume) if issubclass(self.assume.__class__, FunctionCall) else [
|
|
937
|
+
{
|
|
938
|
+
"id": self._ref_or_value(assum_id),
|
|
939
|
+
"bound": self._ref_or_bound(assum_bound)
|
|
940
|
+
}
|
|
941
|
+
for assum_id, assum_bound in self.assume.items()
|
|
942
|
+
],
|
|
943
|
+
"maximize": self.maximize
|
|
944
|
+
},
|
|
945
|
+
"out": self.out
|
|
946
|
+
}]
|
|
947
|
+
|
|
948
|
+
@dataclass(eq=False)
|
|
949
|
+
class get_node_children(FunctionCall):
|
|
950
|
+
|
|
951
|
+
id: RefOrId
|
|
952
|
+
|
|
953
|
+
def json_output(self) -> list:
|
|
954
|
+
build_id = self.id.json_output() if issubclass(self.id.__class__, FunctionCall) else []
|
|
955
|
+
return build_id + [
|
|
956
|
+
{
|
|
957
|
+
"fn": self._fn(),
|
|
958
|
+
"args": {
|
|
959
|
+
"id": self._ref_or_value(self.id)
|
|
960
|
+
},
|
|
961
|
+
"out": self.out
|
|
962
|
+
}
|
|
963
|
+
]
|
|
964
|
+
|
|
965
|
+
@dataclass(eq=False)
|
|
966
|
+
class get_node_parents(FunctionCall):
|
|
967
|
+
|
|
968
|
+
id: RefOrId
|
|
969
|
+
|
|
970
|
+
def json_output(self) -> list:
|
|
971
|
+
build_id = self.id.json_output() if issubclass(self.id.__class__, FunctionCall) else []
|
|
972
|
+
return build_id + [
|
|
973
|
+
{
|
|
974
|
+
"fn": self._fn(),
|
|
975
|
+
"args": {
|
|
976
|
+
"id": self._ref_or_value(self.id)
|
|
977
|
+
},
|
|
978
|
+
"out": self.out
|
|
979
|
+
}
|
|
980
|
+
]
|
|
981
|
+
|
|
982
|
+
class OatDBError(Exception):
|
|
983
|
+
"""Base exception for OatDB client errors."""
|
|
984
|
+
pass
|
|
985
|
+
|
|
986
|
+
class OatDBConnectionError(OatDBError):
|
|
987
|
+
"""Raised when connection to OatDB fails."""
|
|
988
|
+
pass
|
|
989
|
+
|
|
990
|
+
class OatDBExecutionError(OatDBError):
|
|
991
|
+
"""Raised when OatDB execution fails."""
|
|
992
|
+
|
|
993
|
+
def __init__(self, message: str, status_code: int, response: dict):
|
|
994
|
+
super().__init__(message)
|
|
995
|
+
self.status_code = status_code
|
|
996
|
+
self.response = response
|
|
997
|
+
|
|
998
|
+
def __str__(self) -> str:
|
|
999
|
+
import json
|
|
1000
|
+
error_details = json.dumps(self.response, indent=2)
|
|
1001
|
+
return f"{super().__str__()}\nError details:\n{error_details}"
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
@dataclass
|
|
1005
|
+
class OatClient:
|
|
1006
|
+
"""
|
|
1007
|
+
Client for interacting with OatDB API.
|
|
1008
|
+
|
|
1009
|
+
Usage:
|
|
1010
|
+
client = OatClient("http://localhost:7061")
|
|
1011
|
+
|
|
1012
|
+
# Build query
|
|
1013
|
+
x = client.set_primitive("x", bound=10j)
|
|
1014
|
+
y = client.set_primitive("y", bound=10j)
|
|
1015
|
+
constraint = client.set_and([x, y])
|
|
1016
|
+
dag = client.sub(constraint)
|
|
1017
|
+
|
|
1018
|
+
# Execute
|
|
1019
|
+
result = client.execute([dag.out])
|
|
1020
|
+
print(result[dag.out])
|
|
1021
|
+
"""
|
|
1022
|
+
base_url: str
|
|
1023
|
+
timeout: int = 30
|
|
1024
|
+
verify_ssl: bool = True
|
|
1025
|
+
|
|
1026
|
+
def health_check(self) -> bool:
|
|
1027
|
+
"""Check if the OatDB server is healthy."""
|
|
1028
|
+
try:
|
|
1029
|
+
response = requests.get(
|
|
1030
|
+
f"{self.base_url}/health",
|
|
1031
|
+
timeout=self.timeout,
|
|
1032
|
+
verify=self.verify_ssl
|
|
1033
|
+
)
|
|
1034
|
+
return response.status_code == 200
|
|
1035
|
+
except requests.RequestException:
|
|
1036
|
+
return False
|
|
1037
|
+
|
|
1038
|
+
def execute(
|
|
1039
|
+
self,
|
|
1040
|
+
call: FunctionCall
|
|
1041
|
+
) -> Dict[str, Any]:
|
|
1042
|
+
"""
|
|
1043
|
+
Execute all buffered calls.
|
|
1044
|
+
|
|
1045
|
+
Args:
|
|
1046
|
+
outputs: List of output variable names to return.
|
|
1047
|
+
If None, returns all outputs.
|
|
1048
|
+
clear_buffer: Whether to clear the buffer after execution
|
|
1049
|
+
|
|
1050
|
+
Returns:
|
|
1051
|
+
Dict mapping output names to their values
|
|
1052
|
+
|
|
1053
|
+
Raises:
|
|
1054
|
+
OatDBConnectionError: If connection to server fails
|
|
1055
|
+
OatDBExecutionError: If server returns an error
|
|
1056
|
+
"""
|
|
1057
|
+
return self.execute_many([call])[call.out]
|
|
1058
|
+
|
|
1059
|
+
def execute_many(
|
|
1060
|
+
self,
|
|
1061
|
+
calls: List[FunctionCall]
|
|
1062
|
+
) -> List[Dict[str, Any]]:
|
|
1063
|
+
"""
|
|
1064
|
+
Execute multiple calls and return their results.
|
|
1065
|
+
|
|
1066
|
+
Args:
|
|
1067
|
+
calls: List of FunctionCall instances to execute.
|
|
1068
|
+
Returns:
|
|
1069
|
+
List of Dicts mapping output names to their values for each call.
|
|
1070
|
+
"""
|
|
1071
|
+
payload = {
|
|
1072
|
+
"calls": self.debug_payload(calls),
|
|
1073
|
+
"outputs": [call.out for call in calls]
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
try:
|
|
1077
|
+
response = requests.post(
|
|
1078
|
+
f"{self.base_url}/call",
|
|
1079
|
+
json=payload,
|
|
1080
|
+
timeout=self.timeout,
|
|
1081
|
+
verify=self.verify_ssl
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
if response.status_code != 200:
|
|
1085
|
+
error_data = response.json() if response.headers.get('content-type') == 'application/json' else {}
|
|
1086
|
+
raise OatDBExecutionError(
|
|
1087
|
+
f"OatDB execution failed: {response.status_code}",
|
|
1088
|
+
response.status_code,
|
|
1089
|
+
error_data
|
|
1090
|
+
)
|
|
1091
|
+
call_map = {call.out: call for call in calls}
|
|
1092
|
+
response_data = response.json()
|
|
1093
|
+
return {
|
|
1094
|
+
key: call_map[key].json_input(value)
|
|
1095
|
+
for key, value in response_data.items()
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
except requests.RequestException as e:
|
|
1099
|
+
raise OatDBConnectionError(f"Failed to connect to OatDB: {e}") from e
|
|
1100
|
+
|
|
1101
|
+
def debug_payload(self, calls: List[FunctionCall]) -> dict:
|
|
1102
|
+
"""
|
|
1103
|
+
Get the JSON payload that would be sent without executing.
|
|
1104
|
+
Useful for debugging.
|
|
1105
|
+
"""
|
|
1106
|
+
return list(unique_everseen(chain(*[call.json_output() for call in calls]), key=lambda x: x.get('out')))
|
|
1107
|
+
|
|
1108
|
+
# Export public API
|
|
1109
|
+
__all__ = [
|
|
1110
|
+
'OatClient'
|
|
1111
|
+
]
|