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