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 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
+ ]