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 }
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.
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
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 ~followthemoney.graph.Node
or an
~followthemoney.graph.Edge
.
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 <followthemoney.graph.Node>
in the graph.
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 <followthemoney.graph.Edge>
in the graph.
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.
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.
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.
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.