graflo 1.1.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.

Potentially problematic release.


This version of graflo might be problematic. Click here for more details.

Files changed (45) hide show
  1. graflo/README.md +18 -0
  2. graflo/__init__.py +39 -0
  3. graflo/architecture/__init__.py +37 -0
  4. graflo/architecture/actor.py +974 -0
  5. graflo/architecture/actor_util.py +425 -0
  6. graflo/architecture/edge.py +295 -0
  7. graflo/architecture/onto.py +374 -0
  8. graflo/architecture/resource.py +161 -0
  9. graflo/architecture/schema.py +136 -0
  10. graflo/architecture/transform.py +292 -0
  11. graflo/architecture/util.py +93 -0
  12. graflo/architecture/vertex.py +277 -0
  13. graflo/caster.py +409 -0
  14. graflo/cli/__init__.py +14 -0
  15. graflo/cli/ingest.py +144 -0
  16. graflo/cli/manage_dbs.py +193 -0
  17. graflo/cli/plot_schema.py +132 -0
  18. graflo/cli/xml2json.py +93 -0
  19. graflo/db/__init__.py +32 -0
  20. graflo/db/arango/__init__.py +16 -0
  21. graflo/db/arango/conn.py +734 -0
  22. graflo/db/arango/query.py +180 -0
  23. graflo/db/arango/util.py +88 -0
  24. graflo/db/connection.py +304 -0
  25. graflo/db/manager.py +104 -0
  26. graflo/db/neo4j/__init__.py +16 -0
  27. graflo/db/neo4j/conn.py +432 -0
  28. graflo/db/util.py +49 -0
  29. graflo/filter/__init__.py +21 -0
  30. graflo/filter/onto.py +400 -0
  31. graflo/logging.conf +22 -0
  32. graflo/onto.py +186 -0
  33. graflo/plot/__init__.py +17 -0
  34. graflo/plot/plotter.py +556 -0
  35. graflo/util/__init__.py +23 -0
  36. graflo/util/chunker.py +739 -0
  37. graflo/util/merge.py +148 -0
  38. graflo/util/misc.py +37 -0
  39. graflo/util/onto.py +63 -0
  40. graflo/util/transform.py +406 -0
  41. graflo-1.1.0.dist-info/METADATA +157 -0
  42. graflo-1.1.0.dist-info/RECORD +45 -0
  43. graflo-1.1.0.dist-info/WHEEL +4 -0
  44. graflo-1.1.0.dist-info/entry_points.txt +5 -0
  45. graflo-1.1.0.dist-info/licenses/LICENSE +126 -0
@@ -0,0 +1,974 @@
1
+ """Actor-based system for graph data transformation and processing.
2
+
3
+ This module implements a system for processing and transforming graph data.
4
+ It provides a flexible framework for defining and executing data transformations through
5
+ a tree of `actors`. The system supports various types of actors:
6
+
7
+ - VertexActor: Processes and transforms vertex data
8
+ - EdgeActor: Handles edge creation and transformation
9
+ - TransformActor: Applies transformations to data
10
+ - DescendActor: Manages hierarchical processing of nested data structures
11
+
12
+ The module uses an action context to maintain state during processing and supports
13
+ both synchronous and asynchronous operations. It integrates with the graph database
14
+ infrastructure to handle vertex and edge operations.
15
+
16
+ Example:
17
+ >>> wrapper = ActorWrapper(vertex="user")
18
+ >>> ctx = ActionContext()
19
+ >>> result = wrapper(ctx, doc={"id": "123", "name": "John"})
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ from abc import ABC, abstractmethod
26
+ from collections import defaultdict
27
+ from functools import reduce
28
+ from pathlib import Path
29
+ from types import MappingProxyType
30
+ from typing import Optional, Type
31
+
32
+ from graflo.architecture.actor_util import (
33
+ add_blank_collections,
34
+ render_edge,
35
+ render_weights,
36
+ )
37
+ from graflo.architecture.edge import Edge, EdgeConfig
38
+ from graflo.architecture.onto import (
39
+ ActionContext,
40
+ GraphEntity,
41
+ LocationIndex,
42
+ VertexRep,
43
+ )
44
+ from graflo.architecture.transform import ProtoTransform, Transform
45
+ from graflo.architecture.vertex import (
46
+ VertexConfig,
47
+ )
48
+ from graflo.util.merge import (
49
+ merge_doc_basis,
50
+ merge_doc_basis_closest_preceding,
51
+ )
52
+ from graflo.util.transform import pick_unique_dict
53
+
54
+ logger = logging.getLogger(__name__)
55
+
56
+
57
+ DESCEND_KEY = "key"
58
+ DRESSING_TRANSFORMED_VALUE_KEY = "__value__"
59
+
60
+
61
+ class Actor(ABC):
62
+ """Abstract base class for all actors in the system.
63
+
64
+ Actors are the fundamental processing units in the graph transformation system.
65
+ Each actor type implements specific functionality for processing graph data.
66
+
67
+ Attributes:
68
+ None (abstract class)
69
+ """
70
+
71
+ @abstractmethod
72
+ def __call__(self, ctx: ActionContext, lindex: LocationIndex, *nargs, **kwargs):
73
+ """Execute the actor's main processing logic.
74
+
75
+ Args:
76
+ ctx: The action context containing the current processing state
77
+ *nargs: Additional positional arguments
78
+ **kwargs: Additional keyword arguments
79
+
80
+ Returns:
81
+ Updated action context
82
+ """
83
+ pass
84
+
85
+ def fetch_important_items(self):
86
+ """Get a dictionary of important items for string representation.
87
+
88
+ Returns:
89
+ dict: Dictionary of important items
90
+ """
91
+ return {}
92
+
93
+ def finish_init(self, **kwargs):
94
+ """Complete initialization of the actor.
95
+
96
+ Args:
97
+ **kwargs: Additional initialization parameters
98
+ """
99
+ pass
100
+
101
+ def init_transforms(self, **kwargs):
102
+ """Initialize transformations for the actor.
103
+
104
+ Args:
105
+ **kwargs: Transformation parameters
106
+ """
107
+ pass
108
+
109
+ def count(self):
110
+ """Get the count of items processed by this actor.
111
+
112
+ Returns:
113
+ int: Number of items
114
+ """
115
+ return 1
116
+
117
+ def _filter_items(self, items):
118
+ """Filter out None and empty items.
119
+
120
+ Args:
121
+ items: Dictionary of items to filter
122
+
123
+ Returns:
124
+ dict: Filtered dictionary
125
+ """
126
+ return {k: v for k, v in items.items() if v is not None and v}
127
+
128
+ def _stringify_items(self, items):
129
+ """Convert items to string representation.
130
+
131
+ Args:
132
+ items: Dictionary of items to stringify
133
+
134
+ Returns:
135
+ dict: Dictionary with stringified values
136
+ """
137
+ return {
138
+ k: ", ".join(list(v)) if isinstance(v, (tuple, list)) else v
139
+ for k, v in items.items()
140
+ }
141
+
142
+ def __str__(self):
143
+ """Get string representation of the actor.
144
+
145
+ Returns:
146
+ str: String representation
147
+ """
148
+ d = self.fetch_important_items()
149
+ d = self._filter_items(d)
150
+ d = self._stringify_items(d)
151
+ d_list = [[k, d[k]] for k in sorted(d)]
152
+ d_list_b = [type(self).__name__] + [": ".join(x) for x in d_list]
153
+ d_list_str = "\n".join(d_list_b)
154
+ return d_list_str
155
+
156
+ __repr__ = __str__
157
+
158
+ def fetch_actors(self, level, edges):
159
+ """Fetch actor information for tree representation.
160
+
161
+ Args:
162
+ level: Current level in the actor tree
163
+ edges: List of edges in the actor tree
164
+
165
+ Returns:
166
+ tuple: (level, actor_type, string_representation, edges)
167
+ """
168
+ return level, type(self), str(self), edges
169
+
170
+
171
+ class VertexActor(Actor):
172
+ """Actor for processing vertex data.
173
+
174
+ This actor handles the processing and transformation of vertex data, including
175
+ field selection.
176
+
177
+ Attributes:
178
+ name: Name of the vertex
179
+ keep_fields: Optional tuple of fields to keep
180
+ vertex_config: Configuration for the vertex
181
+ """
182
+
183
+ def __init__(
184
+ self,
185
+ vertex: str,
186
+ keep_fields: tuple[str, ...] | None = None,
187
+ **kwargs,
188
+ ):
189
+ """Initialize the vertex actor.
190
+
191
+ Args:
192
+ vertex: Name of the vertex
193
+ keep_fields: Optional tuple of fields to keep
194
+ **kwargs: Additional initialization parameters
195
+ """
196
+ self.name = vertex
197
+ self.keep_fields: tuple[str, ...] | None = keep_fields
198
+ self.vertex_config: VertexConfig
199
+
200
+ def fetch_important_items(self):
201
+ """Get important items for string representation.
202
+
203
+ Returns:
204
+ dict: Dictionary of important items
205
+ """
206
+ sd = self.__dict__
207
+ return {k: sd[k] for k in ["name", "keep_fields"]}
208
+
209
+ def finish_init(self, **kwargs):
210
+ """Complete initialization of the vertex actor.
211
+
212
+ Args:
213
+ **kwargs: Additional initialization parameters
214
+ """
215
+ self.vertex_config: VertexConfig = kwargs.pop("vertex_config")
216
+
217
+ def __call__(self, ctx: ActionContext, lindex: LocationIndex, *nargs, **kwargs):
218
+ """Process vertex data.
219
+
220
+ Args:
221
+ ctx: Action context
222
+ *nargs: Additional positional arguments
223
+ **kwargs: Additional keyword arguments including 'doc'
224
+
225
+ Returns:
226
+ Updated action context
227
+ """
228
+ doc: dict = kwargs.pop("doc", {})
229
+
230
+ vertex_keys = self.vertex_config.fields(self.name, with_aux=True)
231
+ buffer_vertex = ctx.buffer_vertex.pop(self.name, [])
232
+
233
+ agg = []
234
+
235
+ for item in ctx.buffer_transforms[lindex]:
236
+ _doc: dict = dict()
237
+ n_value_keys = len(
238
+ [k for k in item if k.startswith(DRESSING_TRANSFORMED_VALUE_KEY)]
239
+ )
240
+ for j in range(n_value_keys):
241
+ vkey = self.vertex_config.index(self.name).fields[j]
242
+ v = item.pop(f"{DRESSING_TRANSFORMED_VALUE_KEY}#{j}")
243
+ _doc[vkey] = v
244
+
245
+ for vkey in set(vertex_keys) - set(_doc):
246
+ v = item.pop(vkey, None)
247
+ if v is not None:
248
+ _doc[vkey] = v
249
+
250
+ if all(cfilter(doc) for cfilter in self.vertex_config.filters(self.name)):
251
+ agg += [_doc]
252
+
253
+ ctx.buffer_transforms[lindex] = [x for x in ctx.buffer_transforms[lindex] if x]
254
+
255
+ for item in buffer_vertex:
256
+ _doc = {k: item[k] for k in vertex_keys if k in item}
257
+
258
+ if all(cfilter(doc) for cfilter in self.vertex_config.filters(self.name)):
259
+ agg += [_doc]
260
+
261
+ remaining_keys = set(vertex_keys) - reduce(
262
+ lambda acc, d: acc | d.keys(), agg, set()
263
+ )
264
+ passthrough_doc = {}
265
+ for k in remaining_keys:
266
+ if k in doc:
267
+ passthrough_doc[k] = doc.pop(k)
268
+ if passthrough_doc:
269
+ agg += [passthrough_doc]
270
+
271
+ merged = merge_doc_basis(
272
+ agg, index_keys=tuple(self.vertex_config.index(self.name).fields)
273
+ )
274
+
275
+ ctx.acc_vertex[self.name][lindex] += [
276
+ VertexRep(
277
+ vertex=m,
278
+ ctx={q: w for q, w in doc.items() if not isinstance(w, (dict, list))},
279
+ )
280
+ for m in merged
281
+ ]
282
+ return ctx
283
+
284
+
285
+ class EdgeActor(Actor):
286
+ """Actor for processing edge data.
287
+
288
+ This actor handles the creation and transformation of edges between vertices,
289
+ including weight calculations and relationship management.
290
+
291
+ Attributes:
292
+ edge: Edge configuration
293
+ vertex_config: Vertex configuration
294
+ """
295
+
296
+ def __init__(
297
+ self,
298
+ **kwargs,
299
+ ):
300
+ """Initialize the edge actor.
301
+
302
+ Args:
303
+ **kwargs: Edge configuration parameters
304
+ """
305
+ self.edge = Edge.from_dict(kwargs)
306
+ self.vertex_config: VertexConfig
307
+
308
+ def fetch_important_items(self):
309
+ """Get important items for string representation.
310
+
311
+ Returns:
312
+ dict: Dictionary of important items
313
+ """
314
+ sd = self.edge.__dict__
315
+ return {k: sd[k] for k in ["source", "target", "match_source", "match_target"]}
316
+
317
+ def finish_init(self, **kwargs):
318
+ """Complete initialization of the edge actor.
319
+
320
+ Args:
321
+ **kwargs: Additional initialization parameters
322
+ """
323
+ self.vertex_config: VertexConfig = kwargs.pop("vertex_config")
324
+ edge_config: Optional[EdgeConfig] = kwargs.pop("edge_config", None)
325
+ if edge_config is not None and self.vertex_config is not None:
326
+ self.edge.finish_init(vertex_config=self.vertex_config)
327
+ edge_config.update_edges(self.edge, vertex_config=self.vertex_config)
328
+
329
+ def __call__(self, ctx: ActionContext, lindex: LocationIndex, *nargs, **kwargs):
330
+ """Process edge data.
331
+
332
+ Args:
333
+ ctx: Action context
334
+ *nargs: Additional positional arguments
335
+ **kwargs: Additional keyword arguments
336
+
337
+ Returns:
338
+ Updated action context
339
+ """
340
+
341
+ ctx = self.merge_vertices(ctx)
342
+ edges = render_edge(self.edge, self.vertex_config, ctx, lindex=lindex)
343
+
344
+ edges = render_weights(
345
+ self.edge,
346
+ self.vertex_config,
347
+ ctx.acc_vertex,
348
+ edges,
349
+ )
350
+
351
+ for relation, v in edges.items():
352
+ ctx.acc_global[self.edge.source, self.edge.target, relation] += v
353
+
354
+ return ctx
355
+
356
+ def merge_vertices(self, ctx) -> ActionContext:
357
+ for vertex, dd in ctx.acc_vertex.items():
358
+ for lindex, vertex_list in dd.items():
359
+ vvv = merge_doc_basis_closest_preceding(
360
+ vertex_list,
361
+ tuple(self.vertex_config.index(vertex).fields),
362
+ )
363
+ # vvv = pick_unique_dict(vvv)
364
+ ctx.acc_vertex[vertex][lindex] = vvv
365
+ return ctx
366
+
367
+
368
+ class TransformActor(Actor):
369
+ """Actor for applying transformations to data.
370
+
371
+ This actor handles the application of transformations to input data, supporting
372
+ both simple and complex transformation scenarios.
373
+
374
+ Attributes:
375
+ _kwargs: Original initialization parameters
376
+ vertex: Optional target vertex
377
+ transforms: Dictionary of available transforms
378
+ name: Transform name
379
+ params: Transform parameters
380
+ t: Transform instance
381
+ """
382
+
383
+ def __init__(self, **kwargs):
384
+ """Initialize the transform actor.
385
+
386
+ Args:
387
+ **kwargs: Transform configuration parameters
388
+ """
389
+ self._kwargs = kwargs
390
+ self.vertex: Optional[str] = kwargs.pop("target_vertex", None)
391
+ self.transforms: dict
392
+ self.name = kwargs.get("name", None)
393
+ self.params = kwargs.get("params", {})
394
+ self.t: Transform = Transform(**kwargs)
395
+
396
+ def fetch_important_items(self):
397
+ """Get important items for string representation.
398
+
399
+ Returns:
400
+ dict: Dictionary of important items
401
+ """
402
+ sd = self.__dict__
403
+ sm = {k: sd[k] for k in ["name", "vertex"]}
404
+ smb = {"t.input": self.t.input, "t.output": self.t.output}
405
+ return {**sm, **smb}
406
+
407
+ def init_transforms(self, **kwargs):
408
+ """Initialize available transforms.
409
+
410
+ Args:
411
+ **kwargs: Transform initialization parameters
412
+ """
413
+ self.transforms = kwargs.pop("transforms", {})
414
+ try:
415
+ pt = ProtoTransform(
416
+ **{
417
+ k: self._kwargs[k]
418
+ for k in ProtoTransform.get_fields_members()
419
+ if k in self._kwargs
420
+ }
421
+ )
422
+ if pt.name is not None and pt._foo is not None:
423
+ if pt.name not in self.transforms:
424
+ self.transforms[pt.name] = pt
425
+ elif pt.params:
426
+ self.transforms[pt.name] = pt
427
+ except Exception:
428
+ pass
429
+
430
+ def finish_init(self, **kwargs):
431
+ """Complete initialization of the transform actor.
432
+
433
+ Args:
434
+ **kwargs: Additional initialization parameters
435
+ """
436
+ self.transforms: dict[str, ProtoTransform] = kwargs.pop("transforms", {})
437
+
438
+ if self.name is not None:
439
+ pt = self.transforms.get(self.name, None)
440
+ if pt is not None:
441
+ self.t._foo = pt._foo
442
+ self.t.module = pt.module
443
+ self.t.foo = pt.foo
444
+ if pt.params and not self.t.params:
445
+ self.t.params = pt.params
446
+ if (
447
+ pt.input
448
+ and not self.t.input
449
+ and pt.output
450
+ and not self.t.output
451
+ ):
452
+ self.t.input = pt.input
453
+ self.t.output = pt.output
454
+ self.t.__post_init__()
455
+
456
+ def __call__(self, ctx: ActionContext, lindex: LocationIndex, *nargs, **kwargs):
457
+ """Apply transformation to input data.
458
+
459
+ Args:
460
+ ctx: Action context
461
+ *nargs: Additional positional arguments
462
+ **kwargs: Additional keyword arguments including 'doc'
463
+
464
+ Returns:
465
+ Updated action context
466
+
467
+ Raises:
468
+ ValueError: If no document is provided
469
+ """
470
+ logging.debug(f"transforms : {id(self.transforms)} {len(self.transforms)}")
471
+
472
+ if kwargs:
473
+ doc: Optional[dict] = kwargs.get("doc")
474
+ elif nargs:
475
+ doc = nargs[0]
476
+ else:
477
+ raise ValueError(f"{type(self).__name__}: doc should be provided")
478
+
479
+ _update_doc: dict
480
+ if isinstance(doc, dict):
481
+ _update_doc = self.t(doc)
482
+ else:
483
+ value = self.t(doc)
484
+ if isinstance(value, tuple):
485
+ _update_doc = {
486
+ f"{DRESSING_TRANSFORMED_VALUE_KEY}#{j}": v
487
+ for j, v in enumerate(value)
488
+ }
489
+ elif isinstance(value, dict):
490
+ _update_doc = value
491
+ else:
492
+ _update_doc = {f"{DRESSING_TRANSFORMED_VALUE_KEY}#0": value}
493
+
494
+ if self.vertex is None:
495
+ ctx.buffer_transforms[lindex] += [_update_doc]
496
+ else:
497
+ ctx.buffer_vertex[self.vertex] += [_update_doc]
498
+ return ctx
499
+
500
+
501
+ class DescendActor(Actor):
502
+ """Actor for processing hierarchical data structures.
503
+
504
+ This actor manages the processing of nested data structures by coordinating
505
+ the execution of child actors.
506
+
507
+ Attributes:
508
+ key: Optional key for accessing nested data
509
+ _descendants: List of child actor wrappers
510
+ """
511
+
512
+ def __init__(self, key: str | None, descendants_kwargs: list, **kwargs):
513
+ """Initialize the descend actor.
514
+
515
+ Args:
516
+ key: Optional key for accessing nested data
517
+ descendants_kwargs: List of child actor configurations
518
+ **kwargs: Additional initialization parameters
519
+ """
520
+ self.key = key
521
+ self._descendants: list[ActorWrapper] = []
522
+ for descendant_kwargs in descendants_kwargs:
523
+ self._descendants += [ActorWrapper(**descendant_kwargs, **kwargs)]
524
+
525
+ def fetch_important_items(self):
526
+ """Get important items for string representation.
527
+
528
+ Returns:
529
+ dict: Dictionary of important items
530
+ """
531
+ sd = self.__dict__
532
+ sm = {k: sd[k] for k in ["key"]}
533
+ return {**sm}
534
+
535
+ def add_descendant(self, d: ActorWrapper):
536
+ """Add a child actor wrapper.
537
+
538
+ Args:
539
+ d: Actor wrapper to add
540
+ """
541
+ self._descendants += [d]
542
+
543
+ def count(self):
544
+ """Get total count of items processed by all descendants.
545
+
546
+ Returns:
547
+ int: Total count
548
+ """
549
+ return sum(d.count() for d in self.descendants)
550
+
551
+ @property
552
+ def descendants(self) -> list[ActorWrapper]:
553
+ """Get sorted list of descendant actors.
554
+
555
+ Returns:
556
+ list[ActorWrapper]: Sorted list of descendant actors
557
+ """
558
+ return sorted(self._descendants, key=lambda x: _NodeTypePriority[type(x.actor)])
559
+
560
+ def init_transforms(self, **kwargs):
561
+ """Initialize transforms for all descendants.
562
+
563
+ Args:
564
+ **kwargs: Transform initialization parameters
565
+ """
566
+ for an in self.descendants:
567
+ an.init_transforms(**kwargs)
568
+
569
+ def finish_init(self, **kwargs):
570
+ """Complete initialization of the descend actor and its descendants.
571
+
572
+ Args:
573
+ **kwargs: Additional initialization parameters
574
+ """
575
+ self.vertex_config: VertexConfig = kwargs.get(
576
+ "vertex_config", VertexConfig(vertices=[])
577
+ )
578
+
579
+ for an in self.descendants:
580
+ an.finish_init(**kwargs)
581
+
582
+ available_fields = set()
583
+ for anw in self.descendants:
584
+ actor = anw.actor
585
+ if isinstance(actor, TransformActor):
586
+ available_fields |= set(list(actor.t.output))
587
+
588
+ present_vertices = [
589
+ anw.actor.name
590
+ for anw in self.descendants
591
+ if isinstance(anw.actor, VertexActor)
592
+ ]
593
+
594
+ for v in present_vertices:
595
+ available_fields -= set(self.vertex_config.fields(v))
596
+
597
+ for v in self.vertex_config.vertex_list:
598
+ intersection = available_fields & set(v.fields)
599
+ if intersection and v.name not in present_vertices:
600
+ new_descendant = ActorWrapper(vertex=v.name)
601
+ new_descendant.finish_init(**kwargs)
602
+ self.add_descendant(new_descendant)
603
+
604
+ logger.debug(
605
+ f"""type, priority: {
606
+ [
607
+ (t.__name__, _NodeTypePriority[t])
608
+ for t in (type(x.actor) for x in self.descendants)
609
+ ]
610
+ }"""
611
+ )
612
+
613
+ def __call__(self, ctx: ActionContext, lindex: LocationIndex, **kwargs):
614
+ """Process hierarchical data structure.
615
+
616
+ Args:
617
+ ctx: Action context
618
+ **kwargs: Additional keyword arguments including 'doc'
619
+
620
+ Returns:
621
+ Updated action context
622
+
623
+ Raises:
624
+ ValueError: If no document is provided
625
+ """
626
+ doc = kwargs.pop("doc")
627
+
628
+ if doc is None:
629
+ raise ValueError(f"{type(self).__name__}: doc should be provided")
630
+
631
+ if not doc:
632
+ return ctx
633
+
634
+ if self.key is not None:
635
+ if isinstance(doc, dict) and self.key in doc:
636
+ doc = doc[self.key]
637
+ else:
638
+ return ctx
639
+
640
+ doc_level = doc if isinstance(doc, list) else [doc]
641
+
642
+ logger.debug(f"{len(doc_level)}")
643
+
644
+ for idoc, sub_doc in enumerate(doc_level):
645
+ logger.debug(f"docs: {idoc + 1}/{len(doc_level)}")
646
+ if isinstance(sub_doc, dict):
647
+ nargs: tuple = tuple()
648
+ kwargs["doc"] = sub_doc
649
+ else:
650
+ nargs = (sub_doc,)
651
+
652
+ # down the tree
653
+ extra_step = (idoc,) if self.key is None else (self.key, idoc)
654
+ for j, anw in enumerate(self.descendants):
655
+ logger.debug(
656
+ f"{type(anw.actor).__name__}: {j + 1}/{len(self.descendants)}"
657
+ )
658
+ ctx = anw(
659
+ ctx,
660
+ lindex.extend(extra_step),
661
+ *nargs,
662
+ **kwargs,
663
+ )
664
+ return ctx
665
+
666
+ def fetch_actors(self, level, edges):
667
+ """Fetch actor information for tree representation.
668
+
669
+ Args:
670
+ level: Current level in the actor tree
671
+ edges: List of edges in the actor tree
672
+
673
+ Returns:
674
+ tuple: (level, actor_type, string_representation, edges)
675
+ """
676
+ label_current = str(self)
677
+ cname_current = type(self)
678
+ hash_current = hash((level, cname_current, label_current))
679
+ logger.info(f"{hash_current}, {level, cname_current, label_current}")
680
+ props_current = {"label": label_current, "class": cname_current, "level": level}
681
+ for d in self.descendants:
682
+ level_a, cname, label_a, edges_a = d.fetch_actors(level + 1, edges)
683
+ hash_a = hash((level_a, cname, label_a))
684
+ props_a = {"label": label_a, "class": cname, "level": level_a}
685
+ edges = [(hash_current, hash_a, props_current, props_a)] + edges_a
686
+ return level, type(self), str(self), edges
687
+
688
+
689
+ _NodeTypePriority: MappingProxyType[Type[Actor], int] = MappingProxyType(
690
+ {
691
+ DescendActor: 10,
692
+ TransformActor: 20,
693
+ VertexActor: 50,
694
+ EdgeActor: 90,
695
+ }
696
+ )
697
+
698
+
699
+ class ActorWrapper:
700
+ """Wrapper class for managing actor instances.
701
+
702
+ This class provides a unified interface for creating and managing different types
703
+ of actors, handling initialization and execution.
704
+
705
+ Attributes:
706
+ actor: The wrapped actor instance
707
+ vertex_config: Vertex configuration
708
+ edge_config: Edge configuration
709
+ """
710
+
711
+ def __init__(self, *args, **kwargs):
712
+ """Initialize the actor wrapper.
713
+
714
+ Args:
715
+ *args: Positional arguments for actor initialization
716
+ **kwargs: Keyword arguments for actor initialization
717
+
718
+ Raises:
719
+ ValueError: If unable to initialize an actor
720
+ """
721
+ self.actor: Actor
722
+ self.vertex_config: VertexConfig
723
+ self.edge_config: EdgeConfig
724
+ if self._try_init_descend(*args, **kwargs):
725
+ pass
726
+ elif self._try_init_transform(**kwargs):
727
+ pass
728
+ elif self._try_init_vertex(**kwargs):
729
+ pass
730
+ elif self._try_init_edge(**kwargs):
731
+ pass
732
+ else:
733
+ raise ValueError(f"Not able to init ActionNodeWrapper with {kwargs}")
734
+
735
+ def init_transforms(self, **kwargs):
736
+ """Initialize transforms for the wrapped actor.
737
+
738
+ Args:
739
+ **kwargs: Transform initialization parameters
740
+ """
741
+ self.actor.init_transforms(**kwargs)
742
+
743
+ def finish_init(self, **kwargs):
744
+ """Complete initialization of the wrapped actor.
745
+
746
+ Args:
747
+ **kwargs: Additional initialization parameters
748
+ """
749
+ kwargs["transforms"]: dict[str, ProtoTransform] = kwargs.get("transforms", {})
750
+ self.actor.init_transforms(**kwargs)
751
+
752
+ self.vertex_config = kwargs.get("vertex_config", VertexConfig(vertices=[]))
753
+ kwargs["vertex_config"] = self.vertex_config
754
+ self.edge_config = kwargs.get("edge_config", EdgeConfig())
755
+ kwargs["edge_config"] = self.edge_config
756
+ self.actor.finish_init(**kwargs)
757
+
758
+ def count(self):
759
+ """Get count of items processed by the wrapped actor.
760
+
761
+ Returns:
762
+ int: Number of items
763
+ """
764
+ return self.actor.count()
765
+
766
+ def _try_init_descend(self, *args, **kwargs) -> bool:
767
+ """Try to initialize a descend actor.
768
+
769
+ Args:
770
+ *args: Positional arguments
771
+ **kwargs: Keyword arguments
772
+
773
+ Returns:
774
+ bool: True if successful, False otherwise
775
+ """
776
+
777
+ descend_key = kwargs.pop(DESCEND_KEY, None)
778
+
779
+ descendants = kwargs.pop("apply", None)
780
+ if descendants is not None:
781
+ if isinstance(descendants, list):
782
+ descendants = descendants
783
+ else:
784
+ descendants = [descendants]
785
+ elif len(args) > 0:
786
+ descendants = list(args)
787
+ else:
788
+ return False
789
+ self.actor = DescendActor(descend_key, descendants_kwargs=descendants, **kwargs)
790
+ return True
791
+
792
+ def _try_init_transform(self, **kwargs) -> bool:
793
+ """Try to initialize a transform actor.
794
+
795
+ Args:
796
+ **kwargs: Keyword arguments
797
+
798
+ Returns:
799
+ bool: True if successful, False otherwise
800
+ """
801
+ try:
802
+ self.actor = TransformActor(**kwargs)
803
+ return True
804
+ except Exception:
805
+ return False
806
+
807
+ def _try_init_vertex(self, **kwargs) -> bool:
808
+ """Try to initialize a vertex actor.
809
+
810
+ Args:
811
+ **kwargs: Keyword arguments
812
+
813
+ Returns:
814
+ bool: True if successful, False otherwise
815
+ """
816
+ try:
817
+ self.actor = VertexActor(**kwargs)
818
+ return True
819
+ except Exception:
820
+ return False
821
+
822
+ def _try_init_edge(self, **kwargs) -> bool:
823
+ """Try to initialize an edge actor.
824
+
825
+ Args:
826
+ **kwargs: Keyword arguments
827
+
828
+ Returns:
829
+ bool: True if successful, False otherwise
830
+ """
831
+ try:
832
+ self.actor = EdgeActor(**kwargs)
833
+ return True
834
+ except Exception:
835
+ return False
836
+
837
+ def __call__(
838
+ self,
839
+ ctx: ActionContext,
840
+ lindex: LocationIndex = LocationIndex(),
841
+ *nargs,
842
+ **kwargs,
843
+ ) -> ActionContext:
844
+ """Execute the wrapped actor.
845
+
846
+ Args:
847
+ ctx: Action context
848
+ *nargs: Additional positional arguments
849
+ **kwargs: Additional keyword arguments
850
+
851
+ Returns:
852
+ Updated action context
853
+ """
854
+ ctx = self.actor(ctx, lindex, *nargs, **kwargs)
855
+ return ctx
856
+
857
+ def normalize_ctx(self, ctx: ActionContext) -> defaultdict[GraphEntity, list]:
858
+ """Normalize the action context.
859
+
860
+ Args:
861
+ ctx: Action context to normalize
862
+
863
+ Returns:
864
+ defaultdict[GraphEntity, list]: Normalized context
865
+ """
866
+
867
+ for edge_id, edge in self.edge_config.edges_items():
868
+ s, t, _ = edge_id
869
+ edges_ids = [k for k in ctx.acc_global if not isinstance(k, str)]
870
+ if not any(s == sp and t == tp for sp, tp, _ in edges_ids):
871
+ extra_edges = render_edge(
872
+ edge=edge, vertex_config=self.vertex_config, ctx=ctx
873
+ )
874
+ extra_edges = render_weights(
875
+ edge,
876
+ self.vertex_config,
877
+ ctx.acc_vertex,
878
+ extra_edges,
879
+ )
880
+
881
+ for relation, v in extra_edges.items():
882
+ ctx.acc_global[s, t, relation] += v
883
+
884
+ for vertex_name, dd in ctx.acc_vertex.items():
885
+ for lindex, vertex_list in dd.items():
886
+ vertex_list = [x.vertex for x in vertex_list]
887
+ vertex_list_updated = merge_doc_basis(
888
+ vertex_list,
889
+ tuple(self.vertex_config.index(vertex_name).fields),
890
+ )
891
+ vertex_list_updated = pick_unique_dict(vertex_list_updated)
892
+
893
+ ctx.acc_global[vertex_name] += vertex_list_updated
894
+
895
+ ctx = add_blank_collections(ctx, self.vertex_config)
896
+
897
+ return ctx.acc_global
898
+
899
+ @classmethod
900
+ def from_dict(cls, data: dict | list):
901
+ """Create an actor wrapper from a dictionary or list.
902
+
903
+ Args:
904
+ data: Dictionary or list containing actor configuration
905
+
906
+ Returns:
907
+ ActorWrapper: New actor wrapper instance
908
+ """
909
+ if isinstance(data, list):
910
+ return cls(*data)
911
+ else:
912
+ return cls(**data)
913
+
914
+ def assemble_tree(self, fig_path: Optional[Path] = None):
915
+ """Assemble and optionally visualize the actor tree.
916
+
917
+ Args:
918
+ fig_path: Optional path to save the visualization
919
+
920
+ Returns:
921
+ Optional[networkx.MultiDiGraph]: Graph representation of the actor tree
922
+ """
923
+ _, _, _, edges = self.fetch_actors(0, [])
924
+ logger.info(f"{len(edges)}")
925
+ try:
926
+ import networkx as nx
927
+ except ImportError as e:
928
+ logger.error(f"not able to import networks {e}")
929
+ return None
930
+ nodes = {}
931
+ g = nx.MultiDiGraph()
932
+ for ha, hb, pa, pb in edges:
933
+ nodes[ha] = pa
934
+ nodes[hb] = pb
935
+ from graflo.plot.plotter import fillcolor_palette
936
+
937
+ map_class2color = {
938
+ DescendActor: fillcolor_palette["green"],
939
+ VertexActor: "orange",
940
+ EdgeActor: fillcolor_palette["violet"],
941
+ TransformActor: fillcolor_palette["blue"],
942
+ }
943
+
944
+ for n, props in nodes.items():
945
+ nodes[n]["fillcolor"] = map_class2color[props["class"]]
946
+ nodes[n]["style"] = "filled"
947
+ nodes[n]["color"] = "brown"
948
+
949
+ edges = [(ha, hb) for ha, hb, _, _ in edges]
950
+ g.add_edges_from(edges)
951
+ g.add_nodes_from(nodes.items())
952
+
953
+ if fig_path is not None:
954
+ ag = nx.nx_agraph.to_agraph(g)
955
+ ag.draw(
956
+ fig_path,
957
+ "pdf",
958
+ prog="dot",
959
+ )
960
+ return None
961
+ else:
962
+ return g
963
+
964
+ def fetch_actors(self, level, edges):
965
+ """Fetch actor information for tree representation.
966
+
967
+ Args:
968
+ level: Current level in the actor tree
969
+ edges: List of edges in the actor tree
970
+
971
+ Returns:
972
+ tuple: (level, actor_type, string_representation, edges)
973
+ """
974
+ return self.actor.fetch_actors(level, edges)