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.
- graflo/README.md +18 -0
- graflo/__init__.py +39 -0
- graflo/architecture/__init__.py +37 -0
- graflo/architecture/actor.py +974 -0
- graflo/architecture/actor_util.py +425 -0
- graflo/architecture/edge.py +295 -0
- graflo/architecture/onto.py +374 -0
- graflo/architecture/resource.py +161 -0
- graflo/architecture/schema.py +136 -0
- graflo/architecture/transform.py +292 -0
- graflo/architecture/util.py +93 -0
- graflo/architecture/vertex.py +277 -0
- graflo/caster.py +409 -0
- graflo/cli/__init__.py +14 -0
- graflo/cli/ingest.py +144 -0
- graflo/cli/manage_dbs.py +193 -0
- graflo/cli/plot_schema.py +132 -0
- graflo/cli/xml2json.py +93 -0
- graflo/db/__init__.py +32 -0
- graflo/db/arango/__init__.py +16 -0
- graflo/db/arango/conn.py +734 -0
- graflo/db/arango/query.py +180 -0
- graflo/db/arango/util.py +88 -0
- graflo/db/connection.py +304 -0
- graflo/db/manager.py +104 -0
- graflo/db/neo4j/__init__.py +16 -0
- graflo/db/neo4j/conn.py +432 -0
- graflo/db/util.py +49 -0
- graflo/filter/__init__.py +21 -0
- graflo/filter/onto.py +400 -0
- graflo/logging.conf +22 -0
- graflo/onto.py +186 -0
- graflo/plot/__init__.py +17 -0
- graflo/plot/plotter.py +556 -0
- graflo/util/__init__.py +23 -0
- graflo/util/chunker.py +739 -0
- graflo/util/merge.py +148 -0
- graflo/util/misc.py +37 -0
- graflo/util/onto.py +63 -0
- graflo/util/transform.py +406 -0
- graflo-1.1.0.dist-info/METADATA +157 -0
- graflo-1.1.0.dist-info/RECORD +45 -0
- graflo-1.1.0.dist-info/WHEEL +4 -0
- graflo-1.1.0.dist-info/entry_points.txt +5 -0
- 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)
|