followthemoney.graph

Converting FtM data to a property graph data model.

This module provides an abstract data object that represents a property graph. This is used by the exporter modules to convert data to a specific output format, like Cypher or NetworkX.

  1"""
  2Converting FtM data to a property graph data model.
  3
  4This module provides an abstract data object that represents a property
  5graph. This is used by the exporter modules to convert data
  6to a specific output format, like Cypher or NetworkX.
  7"""
  8import logging
  9from typing import Any, Dict, Generator, Iterable, List, Optional
 10
 11from followthemoney.types import registry
 12from followthemoney.types.common import PropertyType
 13from followthemoney.schema import Schema
 14from followthemoney.proxy import EntityProxy
 15from followthemoney.property import Property
 16from followthemoney.exc import InvalidModel
 17
 18log = logging.getLogger(__name__)
 19
 20
 21class Node(object):
 22    """A node represents either an entity that can be rendered as a
 23    node in a graph, or as a re-ified value, like a name, email
 24    address or phone number."""
 25
 26    __slots__ = ["type", "value", "id", "proxy", "schema"]
 27
 28    def __init__(
 29        self,
 30        type_: PropertyType,
 31        value: str,
 32        proxy: Optional[EntityProxy] = None,
 33        schema: Optional[Schema] = None,
 34    ) -> None:
 35        self.type = type_
 36        self.value = value
 37        # _id = type_.node_id_safe(value)
 38        # if _id is None:
 39        #     raise InvalidData("No ID for node")
 40        self.id = type_.node_id_safe(value)
 41        self.proxy = proxy
 42        self.schema = schema if proxy is None else proxy.schema
 43
 44    @property
 45    def is_entity(self) -> bool:
 46        """Check to see if the node represents an entity. If this is false, the
 47        node represents a non-entity property value that has been reified, like
 48        a phone number or a name."""
 49        return self.type == registry.entity
 50
 51    @property
 52    def caption(self) -> str:
 53        """A user-facing label for the current node."""
 54        if self.type == registry.entity and self.proxy is not None:
 55            return self.proxy.caption
 56        caption = self.type.caption(self.value)
 57        return caption or self.value
 58
 59    def to_dict(self) -> Dict[str, Any]:
 60        """Return a simple dictionary to reflect this graph node."""
 61        return {
 62            "id": self.id,
 63            "type": self.type.name,
 64            "value": self.value,
 65            "caption": self.caption,
 66        }
 67
 68    @classmethod
 69    def from_proxy(cls, proxy: EntityProxy) -> "Node":
 70        """For a given :class:`~followthemoney.proxy.EntityProxy`, return a node
 71        based on the entity."""
 72        return cls(registry.entity, proxy.id, proxy=proxy)
 73
 74    def __str__(self) -> str:
 75        return self.caption
 76
 77    def __repr__(self) -> str:
 78        return "<Node(%r, %r, %r)>" % (self.id, self.type, self.caption)
 79
 80    def __hash__(self) -> int:
 81        return hash(self.id)
 82
 83    def __eq__(self, other: Any) -> bool:
 84        return bool(self.id == other.id)
 85
 86
 87class Edge(object):
 88    """A link between two nodes."""
 89
 90    __slots__ = [
 91        "id",
 92        "weight",
 93        "source_id",
 94        "target_id",
 95        "prop",
 96        "proxy",
 97        "schema",
 98        "graph",
 99    ]
100
101    def __init__(
102        self,
103        graph: "Graph",
104        source: Node,
105        target: Node,
106        proxy: Optional[EntityProxy] = None,
107        prop: Optional[Property] = None,
108        value: Optional[str] = None,
109    ):
110        self.graph = graph
111        self.id = f"{source.id}<>{target.id}"
112        self.source_id = source.id
113        self.target_id = target.id
114        self.weight = 1.0
115        self.prop = prop
116        self.proxy = proxy
117        self.schema: Optional[Schema] = None
118        if prop is not None and value is not None:
119            self.weight = prop.specificity(value)
120        if proxy is not None:
121            self.id = f"{source.id}<{proxy.id}>{target.id}"
122            self.schema = proxy.schema
123
124    @property
125    def source(self) -> Optional[Node]:
126        """The graph node from which the edge originates."""
127        if self.source_id is None:
128            return None
129        return self.graph.nodes.get(self.source_id)
130
131    @property
132    def source_prop(self) -> Property:
133        """Get the entity property originating this edge."""
134        if self.schema is not None and self.schema.source_prop is not None:
135            if self.schema.source_prop.reverse is not None:
136                return self.schema.source_prop.reverse
137        if self.prop is None:
138            raise InvalidModel("Contradiction: %r" % self)
139        return self.prop
140
141    @property
142    def target(self) -> Optional[Node]:
143        """The graph node to which the edge points."""
144        if self.target_id is None:
145            return None
146        return self.graph.nodes.get(self.target_id)
147
148    @property
149    def target_prop(self) -> Optional[Property]:
150        """Get the entity property originating this edge."""
151        if self.schema is not None and self.schema.target_prop is not None:
152            return self.schema.target_prop.reverse
153        if self.prop is not None:
154            return self.prop.reverse
155        # NOTE: this edge points at a value node.
156        return None
157
158    @property
159    def type_name(self) -> str:
160        """Return a machine-readable description of the type of the edge.
161        This is either a property name or a schema name."""
162        if self.schema is not None:
163            return self.schema.name
164        if self.prop is None:
165            raise InvalidModel("Invalid edge: %r" % self)
166        return self.prop.name
167
168    def to_dict(self) -> Dict[str, Optional[str]]:
169        return {
170            "id": self.id,
171            "source_id": self.source_id,
172            "target_id": self.target_id,
173            "type_name": self.type_name,
174        }
175
176    def __repr__(self) -> str:
177        return "<Edge(%r)>" % self.id
178
179    def __hash__(self) -> int:
180        return hash(self.id)
181
182    def __eq__(self, other: Any) -> bool:
183        return bool(self.id == other.id)
184
185
186class Graph(object):
187    """A set of nodes and edges, derived from entities and their properties.
188    This represents an alternative interpretation of FtM data as a property
189    graph.
190
191    This class is meant to be extensible in order to support additional
192    backends, like Aleph.
193    """
194
195    def __init__(self, edge_types: Iterable[PropertyType] = registry.pivots) -> None:
196        types = registry.get_types(edge_types)
197        self.edge_types = [t for t in types if t.matchable]
198        self.flush()
199
200    def flush(self) -> None:
201        """Remove all nodes, edges and proxies from the graph."""
202        self.edges: Dict[str, Edge] = {}
203        self.nodes: Dict[str, Node] = {}
204        self.proxies: Dict[str, Optional[EntityProxy]] = {}
205
206    def queue(self, id_: str, proxy: Optional[EntityProxy] = None) -> None:
207        """Register a reference to an entity in the graph."""
208        if id_ not in self.proxies or proxy is not None:
209            self.proxies[id_] = proxy
210
211    @property
212    def queued(self) -> List[str]:
213        """Return a list of all the entities which are referenced from the graph
214        but that haven't been loaded yet. This can be used to get a list of
215        entities that should be included to expand the whole graph by one degree.
216        """
217        return [i for (i, p) in self.proxies.items() if p is None]
218
219    def _get_node_stub(self, prop: Property, value: str) -> Node:
220        if prop.type == registry.entity:
221            self.queue(value)
222        node = Node(prop.type, value, schema=prop.range)
223        if node.id is None:
224            return node
225        if node.id not in self.nodes:
226            self.nodes[node.id] = node
227        return self.nodes[node.id]
228
229    def _add_edge(self, proxy: EntityProxy, source: str, target: str) -> None:
230        if proxy.schema.source_prop is None:
231            raise InvalidModel("Invalid edge entity: %r" % proxy)
232        source_node = self._get_node_stub(proxy.schema.source_prop, source)
233        if proxy.schema.target_prop is None:
234            raise InvalidModel("Invalid edge entity: %r" % proxy)
235        target_node = self._get_node_stub(proxy.schema.target_prop, target)
236        if source_node.id is not None and target_node.id is not None:
237            edge = Edge(self, source_node, target_node, proxy=proxy)
238            self.edges[edge.id] = edge
239
240    def _add_node(self, proxy: EntityProxy) -> None:
241        """Derive a node and its value edges from the given proxy."""
242        entity = Node.from_proxy(proxy)
243        if entity.id is not None:
244            self.nodes[entity.id] = entity
245        for prop, value in proxy.itervalues():
246            if prop.type not in self.edge_types:
247                continue
248            node = self._get_node_stub(prop, value)
249            if node.id is None:
250                continue
251            edge = Edge(self, entity, node, prop=prop, value=value)
252            if edge.weight > 0:
253                self.edges[edge.id] = edge
254
255    def add(self, proxy: EntityProxy) -> None:
256        """Add an :class:`~followthemoney.proxy.EntityProxy` to the graph and make
257        it either a :class:`~followthemoney.graph.Node` or an
258        :class:`~followthemoney.graph.Edge`."""
259        if proxy is None:
260            return
261        self.queue(proxy.id, proxy)
262        if proxy.schema.edge:
263            for (source, target) in proxy.edgepairs():
264                self._add_edge(proxy, source, target)
265        else:
266            self._add_node(proxy)
267
268    def iternodes(self) -> Iterable[Node]:
269        """Iterate all :class:`nodes <followthemoney.graph.Node>` in the graph."""
270        return self.nodes.values()
271
272    def iteredges(self) -> Iterable[Edge]:
273        """Iterate all :class:`edges <followthemoney.graph.Edge>` in the graph."""
274        return self.edges.values()
275
276    def get_outbound(
277        self, node: Node, prop: Optional[Property] = None
278    ) -> Generator[Edge, None, None]:
279        """Get all edges pointed out from the given node."""
280        for edge in self.iteredges():
281            if edge.source == node:
282                if prop and edge.source_prop != prop:
283                    continue
284                yield edge
285
286    def get_inbound(
287        self, node: Node, prop: Optional[Property] = None
288    ) -> Generator[Edge, None, None]:
289        """Get all edges pointed at the given node."""
290        for edge in self.iteredges():
291            if edge.target == node:
292                if prop and edge.target_prop != prop:
293                    continue
294                yield edge
295
296    def get_adjacent(
297        self, node: Node, prop: Optional[Property] = None
298    ) -> Generator[Edge, None, None]:
299        "Get all adjacent edges of the given node."
300        yield from self.get_outbound(node, prop=prop)
301        yield from self.get_inbound(node, prop=prop)
302
303    def to_dict(self) -> Dict[str, Any]:
304        """Return a dictionary with the graph nodes and edges."""
305        return {
306            "nodes": [n.to_dict() for n in self.iternodes()],
307            "edges": [e.to_dict() for e in self.iteredges()],
308        }
log = <Logger followthemoney.graph (WARNING)>
class Node:
22class Node(object):
23    """A node represents either an entity that can be rendered as a
24    node in a graph, or as a re-ified value, like a name, email
25    address or phone number."""
26
27    __slots__ = ["type", "value", "id", "proxy", "schema"]
28
29    def __init__(
30        self,
31        type_: PropertyType,
32        value: str,
33        proxy: Optional[EntityProxy] = None,
34        schema: Optional[Schema] = None,
35    ) -> None:
36        self.type = type_
37        self.value = value
38        # _id = type_.node_id_safe(value)
39        # if _id is None:
40        #     raise InvalidData("No ID for node")
41        self.id = type_.node_id_safe(value)
42        self.proxy = proxy
43        self.schema = schema if proxy is None else proxy.schema
44
45    @property
46    def is_entity(self) -> bool:
47        """Check to see if the node represents an entity. If this is false, the
48        node represents a non-entity property value that has been reified, like
49        a phone number or a name."""
50        return self.type == registry.entity
51
52    @property
53    def caption(self) -> str:
54        """A user-facing label for the current node."""
55        if self.type == registry.entity and self.proxy is not None:
56            return self.proxy.caption
57        caption = self.type.caption(self.value)
58        return caption or self.value
59
60    def to_dict(self) -> Dict[str, Any]:
61        """Return a simple dictionary to reflect this graph node."""
62        return {
63            "id": self.id,
64            "type": self.type.name,
65            "value": self.value,
66            "caption": self.caption,
67        }
68
69    @classmethod
70    def from_proxy(cls, proxy: EntityProxy) -> "Node":
71        """For a given :class:`~followthemoney.proxy.EntityProxy`, return a node
72        based on the entity."""
73        return cls(registry.entity, proxy.id, proxy=proxy)
74
75    def __str__(self) -> str:
76        return self.caption
77
78    def __repr__(self) -> str:
79        return "<Node(%r, %r, %r)>" % (self.id, self.type, self.caption)
80
81    def __hash__(self) -> int:
82        return hash(self.id)
83
84    def __eq__(self, other: Any) -> bool:
85        return bool(self.id == other.id)

A node represents either an entity that can be rendered as a node in a graph, or as a re-ified value, like a name, email address or phone number.

Node( type_: followthemoney.types.common.PropertyType, value: str, proxy: Optional[followthemoney.proxy.EntityProxy] = None, schema: Optional[followthemoney.schema.Schema] = None)
29    def __init__(
30        self,
31        type_: PropertyType,
32        value: str,
33        proxy: Optional[EntityProxy] = None,
34        schema: Optional[Schema] = None,
35    ) -> None:
36        self.type = type_
37        self.value = value
38        # _id = type_.node_id_safe(value)
39        # if _id is None:
40        #     raise InvalidData("No ID for node")
41        self.id = type_.node_id_safe(value)
42        self.proxy = proxy
43        self.schema = schema if proxy is None else proxy.schema
type
value
id
proxy
schema
is_entity: bool
45    @property
46    def is_entity(self) -> bool:
47        """Check to see if the node represents an entity. If this is false, the
48        node represents a non-entity property value that has been reified, like
49        a phone number or a name."""
50        return self.type == registry.entity

Check to see if the node represents an entity. If this is false, the node represents a non-entity property value that has been reified, like a phone number or a name.

caption: str
52    @property
53    def caption(self) -> str:
54        """A user-facing label for the current node."""
55        if self.type == registry.entity and self.proxy is not None:
56            return self.proxy.caption
57        caption = self.type.caption(self.value)
58        return caption or self.value

A user-facing label for the current node.

def to_dict(self) -> Dict[str, Any]:
60    def to_dict(self) -> Dict[str, Any]:
61        """Return a simple dictionary to reflect this graph node."""
62        return {
63            "id": self.id,
64            "type": self.type.name,
65            "value": self.value,
66            "caption": self.caption,
67        }

Return a simple dictionary to reflect this graph node.

@classmethod
def from_proxy( cls, proxy: followthemoney.proxy.EntityProxy) -> Node:
69    @classmethod
70    def from_proxy(cls, proxy: EntityProxy) -> "Node":
71        """For a given :class:`~followthemoney.proxy.EntityProxy`, return a node
72        based on the entity."""
73        return cls(registry.entity, proxy.id, proxy=proxy)

For a given ~followthemoney.proxy.EntityProxy, return a node based on the entity.

class Edge:
 88class Edge(object):
 89    """A link between two nodes."""
 90
 91    __slots__ = [
 92        "id",
 93        "weight",
 94        "source_id",
 95        "target_id",
 96        "prop",
 97        "proxy",
 98        "schema",
 99        "graph",
100    ]
101
102    def __init__(
103        self,
104        graph: "Graph",
105        source: Node,
106        target: Node,
107        proxy: Optional[EntityProxy] = None,
108        prop: Optional[Property] = None,
109        value: Optional[str] = None,
110    ):
111        self.graph = graph
112        self.id = f"{source.id}<>{target.id}"
113        self.source_id = source.id
114        self.target_id = target.id
115        self.weight = 1.0
116        self.prop = prop
117        self.proxy = proxy
118        self.schema: Optional[Schema] = None
119        if prop is not None and value is not None:
120            self.weight = prop.specificity(value)
121        if proxy is not None:
122            self.id = f"{source.id}<{proxy.id}>{target.id}"
123            self.schema = proxy.schema
124
125    @property
126    def source(self) -> Optional[Node]:
127        """The graph node from which the edge originates."""
128        if self.source_id is None:
129            return None
130        return self.graph.nodes.get(self.source_id)
131
132    @property
133    def source_prop(self) -> Property:
134        """Get the entity property originating this edge."""
135        if self.schema is not None and self.schema.source_prop is not None:
136            if self.schema.source_prop.reverse is not None:
137                return self.schema.source_prop.reverse
138        if self.prop is None:
139            raise InvalidModel("Contradiction: %r" % self)
140        return self.prop
141
142    @property
143    def target(self) -> Optional[Node]:
144        """The graph node to which the edge points."""
145        if self.target_id is None:
146            return None
147        return self.graph.nodes.get(self.target_id)
148
149    @property
150    def target_prop(self) -> Optional[Property]:
151        """Get the entity property originating this edge."""
152        if self.schema is not None and self.schema.target_prop is not None:
153            return self.schema.target_prop.reverse
154        if self.prop is not None:
155            return self.prop.reverse
156        # NOTE: this edge points at a value node.
157        return None
158
159    @property
160    def type_name(self) -> str:
161        """Return a machine-readable description of the type of the edge.
162        This is either a property name or a schema name."""
163        if self.schema is not None:
164            return self.schema.name
165        if self.prop is None:
166            raise InvalidModel("Invalid edge: %r" % self)
167        return self.prop.name
168
169    def to_dict(self) -> Dict[str, Optional[str]]:
170        return {
171            "id": self.id,
172            "source_id": self.source_id,
173            "target_id": self.target_id,
174            "type_name": self.type_name,
175        }
176
177    def __repr__(self) -> str:
178        return "<Edge(%r)>" % self.id
179
180    def __hash__(self) -> int:
181        return hash(self.id)
182
183    def __eq__(self, other: Any) -> bool:
184        return bool(self.id == other.id)

A link between two nodes.

Edge( graph: Graph, source: Node, target: Node, proxy: Optional[followthemoney.proxy.EntityProxy] = None, prop: Optional[followthemoney.property.Property] = None, value: Optional[str] = None)
102    def __init__(
103        self,
104        graph: "Graph",
105        source: Node,
106        target: Node,
107        proxy: Optional[EntityProxy] = None,
108        prop: Optional[Property] = None,
109        value: Optional[str] = None,
110    ):
111        self.graph = graph
112        self.id = f"{source.id}<>{target.id}"
113        self.source_id = source.id
114        self.target_id = target.id
115        self.weight = 1.0
116        self.prop = prop
117        self.proxy = proxy
118        self.schema: Optional[Schema] = None
119        if prop is not None and value is not None:
120            self.weight = prop.specificity(value)
121        if proxy is not None:
122            self.id = f"{source.id}<{proxy.id}>{target.id}"
123            self.schema = proxy.schema
graph
id
source_id
target_id
weight
prop
proxy
schema: Optional[followthemoney.schema.Schema]
source: Optional[Node]
125    @property
126    def source(self) -> Optional[Node]:
127        """The graph node from which the edge originates."""
128        if self.source_id is None:
129            return None
130        return self.graph.nodes.get(self.source_id)

The graph node from which the edge originates.

source_prop: followthemoney.property.Property
132    @property
133    def source_prop(self) -> Property:
134        """Get the entity property originating this edge."""
135        if self.schema is not None and self.schema.source_prop is not None:
136            if self.schema.source_prop.reverse is not None:
137                return self.schema.source_prop.reverse
138        if self.prop is None:
139            raise InvalidModel("Contradiction: %r" % self)
140        return self.prop

Get the entity property originating this edge.

target: Optional[Node]
142    @property
143    def target(self) -> Optional[Node]:
144        """The graph node to which the edge points."""
145        if self.target_id is None:
146            return None
147        return self.graph.nodes.get(self.target_id)

The graph node to which the edge points.

target_prop: Optional[followthemoney.property.Property]
149    @property
150    def target_prop(self) -> Optional[Property]:
151        """Get the entity property originating this edge."""
152        if self.schema is not None and self.schema.target_prop is not None:
153            return self.schema.target_prop.reverse
154        if self.prop is not None:
155            return self.prop.reverse
156        # NOTE: this edge points at a value node.
157        return None

Get the entity property originating this edge.

type_name: str
159    @property
160    def type_name(self) -> str:
161        """Return a machine-readable description of the type of the edge.
162        This is either a property name or a schema name."""
163        if self.schema is not None:
164            return self.schema.name
165        if self.prop is None:
166            raise InvalidModel("Invalid edge: %r" % self)
167        return self.prop.name

Return a machine-readable description of the type of the edge. This is either a property name or a schema name.

def to_dict(self) -> Dict[str, Optional[str]]:
169    def to_dict(self) -> Dict[str, Optional[str]]:
170        return {
171            "id": self.id,
172            "source_id": self.source_id,
173            "target_id": self.target_id,
174            "type_name": self.type_name,
175        }
class Graph:
187class Graph(object):
188    """A set of nodes and edges, derived from entities and their properties.
189    This represents an alternative interpretation of FtM data as a property
190    graph.
191
192    This class is meant to be extensible in order to support additional
193    backends, like Aleph.
194    """
195
196    def __init__(self, edge_types: Iterable[PropertyType] = registry.pivots) -> None:
197        types = registry.get_types(edge_types)
198        self.edge_types = [t for t in types if t.matchable]
199        self.flush()
200
201    def flush(self) -> None:
202        """Remove all nodes, edges and proxies from the graph."""
203        self.edges: Dict[str, Edge] = {}
204        self.nodes: Dict[str, Node] = {}
205        self.proxies: Dict[str, Optional[EntityProxy]] = {}
206
207    def queue(self, id_: str, proxy: Optional[EntityProxy] = None) -> None:
208        """Register a reference to an entity in the graph."""
209        if id_ not in self.proxies or proxy is not None:
210            self.proxies[id_] = proxy
211
212    @property
213    def queued(self) -> List[str]:
214        """Return a list of all the entities which are referenced from the graph
215        but that haven't been loaded yet. This can be used to get a list of
216        entities that should be included to expand the whole graph by one degree.
217        """
218        return [i for (i, p) in self.proxies.items() if p is None]
219
220    def _get_node_stub(self, prop: Property, value: str) -> Node:
221        if prop.type == registry.entity:
222            self.queue(value)
223        node = Node(prop.type, value, schema=prop.range)
224        if node.id is None:
225            return node
226        if node.id not in self.nodes:
227            self.nodes[node.id] = node
228        return self.nodes[node.id]
229
230    def _add_edge(self, proxy: EntityProxy, source: str, target: str) -> None:
231        if proxy.schema.source_prop is None:
232            raise InvalidModel("Invalid edge entity: %r" % proxy)
233        source_node = self._get_node_stub(proxy.schema.source_prop, source)
234        if proxy.schema.target_prop is None:
235            raise InvalidModel("Invalid edge entity: %r" % proxy)
236        target_node = self._get_node_stub(proxy.schema.target_prop, target)
237        if source_node.id is not None and target_node.id is not None:
238            edge = Edge(self, source_node, target_node, proxy=proxy)
239            self.edges[edge.id] = edge
240
241    def _add_node(self, proxy: EntityProxy) -> None:
242        """Derive a node and its value edges from the given proxy."""
243        entity = Node.from_proxy(proxy)
244        if entity.id is not None:
245            self.nodes[entity.id] = entity
246        for prop, value in proxy.itervalues():
247            if prop.type not in self.edge_types:
248                continue
249            node = self._get_node_stub(prop, value)
250            if node.id is None:
251                continue
252            edge = Edge(self, entity, node, prop=prop, value=value)
253            if edge.weight > 0:
254                self.edges[edge.id] = edge
255
256    def add(self, proxy: EntityProxy) -> None:
257        """Add an :class:`~followthemoney.proxy.EntityProxy` to the graph and make
258        it either a :class:`~followthemoney.graph.Node` or an
259        :class:`~followthemoney.graph.Edge`."""
260        if proxy is None:
261            return
262        self.queue(proxy.id, proxy)
263        if proxy.schema.edge:
264            for (source, target) in proxy.edgepairs():
265                self._add_edge(proxy, source, target)
266        else:
267            self._add_node(proxy)
268
269    def iternodes(self) -> Iterable[Node]:
270        """Iterate all :class:`nodes <followthemoney.graph.Node>` in the graph."""
271        return self.nodes.values()
272
273    def iteredges(self) -> Iterable[Edge]:
274        """Iterate all :class:`edges <followthemoney.graph.Edge>` in the graph."""
275        return self.edges.values()
276
277    def get_outbound(
278        self, node: Node, prop: Optional[Property] = None
279    ) -> Generator[Edge, None, None]:
280        """Get all edges pointed out from the given node."""
281        for edge in self.iteredges():
282            if edge.source == node:
283                if prop and edge.source_prop != prop:
284                    continue
285                yield edge
286
287    def get_inbound(
288        self, node: Node, prop: Optional[Property] = None
289    ) -> Generator[Edge, None, None]:
290        """Get all edges pointed at the given node."""
291        for edge in self.iteredges():
292            if edge.target == node:
293                if prop and edge.target_prop != prop:
294                    continue
295                yield edge
296
297    def get_adjacent(
298        self, node: Node, prop: Optional[Property] = None
299    ) -> Generator[Edge, None, None]:
300        "Get all adjacent edges of the given node."
301        yield from self.get_outbound(node, prop=prop)
302        yield from self.get_inbound(node, prop=prop)
303
304    def to_dict(self) -> Dict[str, Any]:
305        """Return a dictionary with the graph nodes and edges."""
306        return {
307            "nodes": [n.to_dict() for n in self.iternodes()],
308            "edges": [e.to_dict() for e in self.iteredges()],
309        }

A set of nodes and edges, derived from entities and their properties. This represents an alternative interpretation of FtM data as a property graph.

This class is meant to be extensible in order to support additional backends, like Aleph.

Graph( edge_types: Iterable[followthemoney.types.common.PropertyType] = {<name>, <address>, <ip>, <checksum>, <iban>, <email>, <entity>, <phone>, <url>, <identifier>})
196    def __init__(self, edge_types: Iterable[PropertyType] = registry.pivots) -> None:
197        types = registry.get_types(edge_types)
198        self.edge_types = [t for t in types if t.matchable]
199        self.flush()
edge_types
def flush(self) -> None:
201    def flush(self) -> None:
202        """Remove all nodes, edges and proxies from the graph."""
203        self.edges: Dict[str, Edge] = {}
204        self.nodes: Dict[str, Node] = {}
205        self.proxies: Dict[str, Optional[EntityProxy]] = {}

Remove all nodes, edges and proxies from the graph.

def queue( self, id_: str, proxy: Optional[followthemoney.proxy.EntityProxy] = None) -> None:
207    def queue(self, id_: str, proxy: Optional[EntityProxy] = None) -> None:
208        """Register a reference to an entity in the graph."""
209        if id_ not in self.proxies or proxy is not None:
210            self.proxies[id_] = proxy

Register a reference to an entity in the graph.

queued: List[str]
212    @property
213    def queued(self) -> List[str]:
214        """Return a list of all the entities which are referenced from the graph
215        but that haven't been loaded yet. This can be used to get a list of
216        entities that should be included to expand the whole graph by one degree.
217        """
218        return [i for (i, p) in self.proxies.items() if p is None]

Return a list of all the entities which are referenced from the graph but that haven't been loaded yet. This can be used to get a list of entities that should be included to expand the whole graph by one degree.

def add(self, proxy: followthemoney.proxy.EntityProxy) -> None:
256    def add(self, proxy: EntityProxy) -> None:
257        """Add an :class:`~followthemoney.proxy.EntityProxy` to the graph and make
258        it either a :class:`~followthemoney.graph.Node` or an
259        :class:`~followthemoney.graph.Edge`."""
260        if proxy is None:
261            return
262        self.queue(proxy.id, proxy)
263        if proxy.schema.edge:
264            for (source, target) in proxy.edgepairs():
265                self._add_edge(proxy, source, target)
266        else:
267            self._add_node(proxy)

Add an ~followthemoney.proxy.EntityProxy to the graph and make it either a ~Node or an ~Edge.

def iternodes(self) -> Iterable[Node]:
269    def iternodes(self) -> Iterable[Node]:
270        """Iterate all :class:`nodes <followthemoney.graph.Node>` in the graph."""
271        return self.nodes.values()

Iterate all nodes <Node> in the graph.

def iteredges(self) -> Iterable[Edge]:
273    def iteredges(self) -> Iterable[Edge]:
274        """Iterate all :class:`edges <followthemoney.graph.Edge>` in the graph."""
275        return self.edges.values()

Iterate all edges <Edge> in the graph.

def get_outbound( self, node: Node, prop: Optional[followthemoney.property.Property] = None) -> Generator[Edge, NoneType, NoneType]:
277    def get_outbound(
278        self, node: Node, prop: Optional[Property] = None
279    ) -> Generator[Edge, None, None]:
280        """Get all edges pointed out from the given node."""
281        for edge in self.iteredges():
282            if edge.source == node:
283                if prop and edge.source_prop != prop:
284                    continue
285                yield edge

Get all edges pointed out from the given node.

def get_inbound( self, node: Node, prop: Optional[followthemoney.property.Property] = None) -> Generator[Edge, NoneType, NoneType]:
287    def get_inbound(
288        self, node: Node, prop: Optional[Property] = None
289    ) -> Generator[Edge, None, None]:
290        """Get all edges pointed at the given node."""
291        for edge in self.iteredges():
292            if edge.target == node:
293                if prop and edge.target_prop != prop:
294                    continue
295                yield edge

Get all edges pointed at the given node.

def get_adjacent( self, node: Node, prop: Optional[followthemoney.property.Property] = None) -> Generator[Edge, NoneType, NoneType]:
297    def get_adjacent(
298        self, node: Node, prop: Optional[Property] = None
299    ) -> Generator[Edge, None, None]:
300        "Get all adjacent edges of the given node."
301        yield from self.get_outbound(node, prop=prop)
302        yield from self.get_inbound(node, prop=prop)

Get all adjacent edges of the given node.

def to_dict(self) -> Dict[str, Any]:
304    def to_dict(self) -> Dict[str, Any]:
305        """Return a dictionary with the graph nodes and edges."""
306        return {
307            "nodes": [n.to_dict() for n in self.iternodes()],
308            "edges": [e.to_dict() for e in self.iteredges()],
309        }

Return a dictionary with the graph nodes and edges.