followthemoney 1.3.7__py3-none-any.whl → 3.8.1__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.
Files changed (186) hide show
  1. followthemoney/__init__.py +5 -3
  2. followthemoney/cli/__init__.py +17 -0
  3. followthemoney/cli/aggregate.py +56 -0
  4. followthemoney/cli/cli.py +88 -0
  5. followthemoney/cli/exports.py +121 -0
  6. followthemoney/cli/mapping.py +85 -0
  7. followthemoney/cli/sieve.py +67 -0
  8. followthemoney/cli/util.py +142 -0
  9. followthemoney/compare.py +130 -60
  10. followthemoney/exc.py +19 -6
  11. followthemoney/export/common.py +29 -0
  12. followthemoney/export/csv.py +82 -0
  13. followthemoney/export/excel.py +75 -0
  14. followthemoney/export/graph.py +79 -0
  15. followthemoney/export/neo4j.py +182 -0
  16. followthemoney/export/rdf.py +26 -0
  17. followthemoney/graph.py +308 -0
  18. followthemoney/helpers.py +212 -0
  19. followthemoney/mapping/__init__.py +1 -1
  20. followthemoney/mapping/csv.py +67 -35
  21. followthemoney/mapping/entity.py +116 -44
  22. followthemoney/mapping/property.py +90 -44
  23. followthemoney/mapping/query.py +27 -19
  24. followthemoney/mapping/source.py +15 -5
  25. followthemoney/mapping/sql.py +75 -61
  26. followthemoney/messages.py +13 -7
  27. followthemoney/model.py +108 -56
  28. followthemoney/namespace.py +119 -0
  29. followthemoney/offshore.py +48 -0
  30. followthemoney/ontology.py +77 -0
  31. followthemoney/property.py +204 -71
  32. followthemoney/proxy.py +455 -118
  33. followthemoney/rdf.py +9 -0
  34. followthemoney/schema/Address.yaml +78 -0
  35. followthemoney/schema/Airplane.yaml +17 -10
  36. followthemoney/schema/Analyzable.yaml +54 -0
  37. followthemoney/schema/Article.yaml +16 -0
  38. followthemoney/schema/Assessment.yaml +32 -0
  39. followthemoney/schema/Asset.yaml +10 -4
  40. followthemoney/schema/Associate.yaml +41 -0
  41. followthemoney/schema/Audio.yaml +24 -0
  42. followthemoney/schema/BankAccount.yaml +53 -9
  43. followthemoney/schema/Call.yaml +48 -0
  44. followthemoney/schema/CallForTenders.yaml +117 -0
  45. followthemoney/schema/Company.yaml +37 -12
  46. followthemoney/schema/Contract.yaml +41 -7
  47. followthemoney/schema/ContractAward.yaml +30 -11
  48. followthemoney/schema/CourtCase.yaml +16 -10
  49. followthemoney/schema/CourtCaseParty.yaml +17 -6
  50. followthemoney/schema/CryptoWallet.yaml +48 -0
  51. followthemoney/schema/Debt.yaml +37 -0
  52. followthemoney/schema/Directorship.yaml +17 -4
  53. followthemoney/schema/Document.yaml +72 -139
  54. followthemoney/schema/Documentation.yml +38 -0
  55. followthemoney/schema/EconomicActivity.yaml +32 -17
  56. followthemoney/schema/Email.yaml +76 -0
  57. followthemoney/schema/Employment.yaml +39 -0
  58. followthemoney/schema/Event.yaml +35 -3
  59. followthemoney/schema/Family.yaml +41 -0
  60. followthemoney/schema/Folder.yaml +13 -0
  61. followthemoney/schema/HyperText.yaml +21 -0
  62. followthemoney/schema/Identification.yaml +40 -0
  63. followthemoney/schema/Image.yaml +25 -0
  64. followthemoney/schema/Interest.yaml +3 -6
  65. followthemoney/schema/Interval.yaml +56 -5
  66. followthemoney/schema/LegalEntity.yaml +81 -20
  67. followthemoney/schema/License.yaml +7 -3
  68. followthemoney/schema/Membership.yaml +19 -4
  69. followthemoney/schema/Mention.yaml +54 -0
  70. followthemoney/schema/Message.yaml +78 -0
  71. followthemoney/schema/Note.yaml +23 -0
  72. followthemoney/schema/Occupancy.yaml +44 -0
  73. followthemoney/schema/Organization.yaml +38 -3
  74. followthemoney/schema/Ownership.yaml +16 -4
  75. followthemoney/schema/Package.yaml +17 -0
  76. followthemoney/schema/Page.yaml +43 -0
  77. followthemoney/schema/Pages.yaml +23 -0
  78. followthemoney/schema/Passport.yaml +16 -17
  79. followthemoney/schema/Payment.yaml +38 -7
  80. followthemoney/schema/Person.yaml +61 -5
  81. followthemoney/schema/PlainText.yaml +17 -0
  82. followthemoney/schema/Position.yaml +50 -0
  83. followthemoney/schema/Post.yaml +42 -0
  84. followthemoney/schema/Project.yaml +27 -0
  85. followthemoney/schema/ProjectParticipant.yaml +36 -0
  86. followthemoney/schema/PublicBody.yaml +14 -3
  87. followthemoney/schema/RealEstate.yaml +19 -3
  88. followthemoney/schema/Representation.yaml +17 -6
  89. followthemoney/schema/Sanction.yaml +45 -21
  90. followthemoney/schema/Security.yaml +59 -0
  91. followthemoney/schema/Similar.yaml +37 -0
  92. followthemoney/schema/Succession.yaml +36 -0
  93. followthemoney/schema/Table.yaml +32 -0
  94. followthemoney/schema/TaxRoll.yaml +27 -9
  95. followthemoney/schema/Thing.yaml +69 -13
  96. followthemoney/schema/Trip.yaml +42 -0
  97. followthemoney/schema/UnknownLink.yaml +17 -6
  98. followthemoney/schema/UserAccount.yaml +44 -0
  99. followthemoney/schema/Value.yaml +5 -1
  100. followthemoney/schema/Vehicle.yaml +25 -8
  101. followthemoney/schema/Vessel.yaml +18 -10
  102. followthemoney/schema/Video.yaml +20 -0
  103. followthemoney/schema/Workbook.yaml +18 -0
  104. followthemoney/schema.py +436 -135
  105. followthemoney/translations/ar/LC_MESSAGES/followthemoney.mo +0 -0
  106. followthemoney/translations/ar/LC_MESSAGES/followthemoney.po +2900 -787
  107. followthemoney/translations/bs/LC_MESSAGES/followthemoney.mo +0 -0
  108. followthemoney/translations/bs/LC_MESSAGES/followthemoney.po +2108 -520
  109. followthemoney/translations/de/LC_MESSAGES/followthemoney.mo +0 -0
  110. followthemoney/translations/de/LC_MESSAGES/followthemoney.po +2902 -782
  111. followthemoney/translations/es/LC_MESSAGES/followthemoney.mo +0 -0
  112. followthemoney/translations/es/LC_MESSAGES/followthemoney.po +2893 -779
  113. followthemoney/translations/fr/LC_MESSAGES/followthemoney.mo +0 -0
  114. followthemoney/translations/fr/LC_MESSAGES/followthemoney.po +4362 -0
  115. followthemoney/translations/fr/followthemoney.po +3861 -0
  116. followthemoney/translations/messages.pot +3021 -725
  117. followthemoney/translations/nb/LC_MESSAGES/followthemoney.mo +0 -0
  118. followthemoney/translations/nb/LC_MESSAGES/followthemoney.po +3778 -0
  119. followthemoney/translations/nl/LC_MESSAGES/followthemoney.mo +0 -0
  120. followthemoney/translations/nl/LC_MESSAGES/followthemoney.po +3837 -0
  121. followthemoney/translations/pt_BR/LC_MESSAGES/followthemoney.mo +0 -0
  122. followthemoney/translations/pt_BR/LC_MESSAGES/followthemoney.po +3784 -0
  123. followthemoney/translations/ru/LC_MESSAGES/followthemoney.mo +0 -0
  124. followthemoney/translations/ru/LC_MESSAGES/followthemoney.po +2837 -539
  125. followthemoney/translations/ru/followthemoney.po +4221 -0
  126. followthemoney/translations/tr/LC_MESSAGES/followthemoney.mo +0 -0
  127. followthemoney/translations/tr/LC_MESSAGES/followthemoney.po +2073 -491
  128. followthemoney/types/__init__.py +35 -17
  129. followthemoney/types/address.py +50 -21
  130. followthemoney/types/checksum.py +25 -0
  131. followthemoney/types/common.py +233 -88
  132. followthemoney/types/country.py +50 -56
  133. followthemoney/types/date.py +59 -76
  134. followthemoney/types/email.py +66 -35
  135. followthemoney/types/entity.py +66 -13
  136. followthemoney/types/gender.py +66 -0
  137. followthemoney/types/iban.py +47 -28
  138. followthemoney/types/identifier.py +49 -22
  139. followthemoney/types/ip.py +35 -21
  140. followthemoney/types/json.py +58 -0
  141. followthemoney/types/language.py +124 -37
  142. followthemoney/types/mimetype.py +44 -0
  143. followthemoney/types/name.py +56 -12
  144. followthemoney/types/number.py +30 -0
  145. followthemoney/types/phone.py +92 -34
  146. followthemoney/types/registry.py +52 -0
  147. followthemoney/types/string.py +43 -0
  148. followthemoney/types/topic.py +94 -0
  149. followthemoney/types/url.py +39 -17
  150. followthemoney/util.py +139 -45
  151. followthemoney-3.8.1.dist-info/METADATA +153 -0
  152. followthemoney-3.8.1.dist-info/RECORD +157 -0
  153. {followthemoney-1.3.7.dist-info → followthemoney-3.8.1.dist-info}/WHEEL +1 -2
  154. followthemoney-3.8.1.dist-info/entry_points.txt +17 -0
  155. followthemoney-1.3.7.dist-info/LICENSE.txt → followthemoney-3.8.1.dist-info/licenses/LICENSE +1 -1
  156. followthemoney/link.py +0 -75
  157. followthemoney/schema/Associate.yml +0 -19
  158. followthemoney/schema/Family.yml +0 -19
  159. followthemoney/schema/Land.yml +0 -9
  160. followthemoney/schema/Relationship.yaml +0 -26
  161. followthemoney/types/domain.py +0 -50
  162. followthemoney-1.3.7.dist-info/DESCRIPTION.rst +0 -3
  163. followthemoney-1.3.7.dist-info/METADATA +0 -39
  164. followthemoney-1.3.7.dist-info/RECORD +0 -108
  165. followthemoney-1.3.7.dist-info/entry_points.txt +0 -3
  166. followthemoney-1.3.7.dist-info/metadata.json +0 -1
  167. followthemoney-1.3.7.dist-info/namespace_packages.txt +0 -1
  168. followthemoney-1.3.7.dist-info/top_level.txt +0 -3
  169. ns/ontology.py +0 -128
  170. tests/types/test_addresses.py +0 -24
  171. tests/types/test_common.py +0 -32
  172. tests/types/test_countries.py +0 -27
  173. tests/types/test_dates.py +0 -73
  174. tests/types/test_domains.py +0 -23
  175. tests/types/test_emails.py +0 -32
  176. tests/types/test_entity.py +0 -19
  177. tests/types/test_iban.py +0 -109
  178. tests/types/test_identifiers.py +0 -27
  179. tests/types/test_ip.py +0 -29
  180. tests/types/test_languages.py +0 -23
  181. tests/types/test_names.py +0 -33
  182. tests/types/test_phones.py +0 -24
  183. tests/types/test_registry.py +0 -14
  184. tests/types/test_urls.py +0 -23
  185. {ns → followthemoney/export}/__init__.py +0 -0
  186. /tests/types/__init__.py → /followthemoney/py.typed +0 -0
@@ -1,96 +1,229 @@
1
- from banal import ensure_list, is_mapping
2
- from rdflib import URIRef
1
+ from banal import is_mapping, as_bool
2
+ from typing import TYPE_CHECKING, cast, Any, List, Optional, TypedDict
3
3
 
4
4
  from followthemoney.exc import InvalidModel
5
5
  from followthemoney.types import registry
6
- from followthemoney.util import gettext, NAMESPACE
6
+ from followthemoney.rdf import NS, URIRef
7
+ from followthemoney.util import gettext, get_entity_id
8
+
9
+ if TYPE_CHECKING:
10
+ from followthemoney.schema import Schema
11
+ from followthemoney.model import Model
12
+
13
+
14
+ class ReverseSpec(TypedDict, total=False):
15
+ name: str
16
+ label: Optional[str]
17
+ hidden: Optional[bool]
18
+
19
+
20
+ class PropertyDict(TypedDict, total=False):
21
+ label: Optional[str]
22
+ description: Optional[str]
23
+ type: Optional[str]
24
+ hidden: Optional[bool]
25
+ matchable: Optional[bool]
26
+ deprecated: Optional[bool]
27
+ maxLength: Optional[int]
28
+ # stub: Optional[bool]
29
+ rdf: Optional[str]
30
+ range: Optional[str]
31
+ format: Optional[str]
32
+
33
+
34
+ class PropertySpec(PropertyDict):
35
+ reverse: ReverseSpec
36
+
37
+
38
+ class PropertyToDict(PropertyDict, total=False):
39
+ name: str
40
+ qname: str
41
+ reverse: Optional[str]
42
+ stub: Optional[bool]
43
+
44
+
45
+ class Property:
46
+ """A definition of a value-holding field on a schema. Properties define
47
+ the field type and other possible constraints. They also serve as entity
48
+ to entity references."""
49
+
50
+ __slots__ = (
51
+ "model",
52
+ "schema",
53
+ "name",
54
+ "qname",
55
+ "_label",
56
+ "_hash",
57
+ "_description",
58
+ "hidden",
59
+ "type",
60
+ "matchable",
61
+ "deprecated",
62
+ "max_length",
63
+ "_range",
64
+ "format",
65
+ "range",
66
+ "stub",
67
+ "_reverse",
68
+ "reverse",
69
+ "uri",
70
+ )
71
+
72
+ #: Invalid property names.
73
+ RESERVED = ["id", "caption", "schema", "schemata"]
74
+
75
+ def __init__(self, schema: "Schema", name: str, data: PropertySpec) -> None:
76
+ #: The schema which the property is defined for. This is always the
77
+ #: most abstract schema that has this property, not the possible
78
+ #: child schemata that inherit it.
79
+ self.schema = schema
7
80
 
81
+ #: Machine-readable name for this property.
82
+ self.name = name
8
83
 
9
- class Property(object):
84
+ #: Qualified property name, which also includes the schema name.
85
+ self.qname = "%s:%s" % (schema.name, self.name)
86
+ if self.name in self.RESERVED:
87
+ raise InvalidModel("Reserved name: %s" % self.name)
10
88
 
11
- def __init__(self, schema, name, data, stub=False):
12
- self.schema = schema
13
- self.name = name.strip()
14
- self.qname = '%s:%s' % (schema.name, self.name)
15
- self.data = data
16
- self._label = data.get('label', name)
17
- self._description = data.get('description')
18
- self.caption = data.get('caption', False)
19
- self.required = data.get('required', False)
20
- self._type = data.get('type', 'text')
21
- self.type = registry.get(self._type)
22
- if self.type is None:
23
- raise InvalidModel("Invalid type: %s" % self._type)
24
-
25
- self.range = None
26
- self.reverse = None
27
- self.stub = stub
28
-
29
- self.uri = NAMESPACE[self.qname]
30
- if 'rdf' in data:
31
- self.uri = URIRef(data.get('rdf'))
32
-
33
- def generate(self):
34
- range_ = self.data.get('schema', 'Thing')
35
- if range_:
36
- self.range = self.schema.model.get(range_)
37
- if self.range is None:
38
- raise InvalidModel("Cannot find range: %s" % self._range)
39
-
40
- reverse_ = self.data.get('reverse')
41
- if self.range and reverse_:
42
- if not is_mapping(reverse_):
43
- raise InvalidModel("Invalid reverse: %s" % self)
44
- self.reverse = self.range._add_reverse(reverse_, self)
89
+ self._hash = hash("<Property(%r)>" % self.qname)
90
+
91
+ self._label = data.get("label", name)
92
+ self._description = data.get("description")
93
+
94
+ #: This property is deprecated and should not be used.
95
+ self.deprecated = as_bool(data.get("deprecated", False))
96
+
97
+ #: This property should not be shown or mentioned in the user interface.
98
+ self.hidden = as_bool(data.get("hidden"))
99
+
100
+ type_ = data.get("type", "string")
101
+ if type_ is None or type_ not in registry.named:
102
+ raise InvalidModel("Invalid type: %s" % type_)
103
+
104
+ #: The data type for this property.
105
+ self.type = registry[type_]
106
+
107
+ #: Whether this property should be used for matching and cross-referencing.
108
+ _matchable = data.get("matchable")
109
+ if _matchable is not None:
110
+ self.matchable = as_bool(data.get("matchable"))
111
+ else:
112
+ self.matchable = self.type.matchable
113
+
114
+ #: The maximum length of the property value.
115
+ self.max_length = int(data.get("maxLength") or self.type.max_length)
116
+
117
+ #: If the property is of type ``entity``, the set of valid schema to be added
118
+ #: in this property can be constrained. For example, an asset can be owned,
119
+ #: but a person cannot be owned.
120
+ self._range = data.get("range")
121
+ self.range: Optional["Schema"] = None
122
+
123
+ #: If the property is of type ``identifier``, a more narrow definition of the
124
+ #: identifier format can be provided. For example, LEI, INN or IBAN codes
125
+ #: can be automatically validated.
126
+ self.format: Optional[str] = data.get("format")
127
+
128
+ #: When a property points to another schema, a reverse property is added for
129
+ #: various administrative reasons. These properties are, however, not real
130
+ #: and cannot be written to. That's why they are marked as stubs and adding
131
+ #: values to them will raise an exception.
132
+ self.stub: Optional[bool] = False
133
+
134
+ #: When a property points to another schema, a stub reverse property is
135
+ #: added as a place to store metadata to help display the link in inverted
136
+ #: views.
137
+ self._reverse = data.get("reverse")
138
+ self.reverse: Optional["Property"] = None
139
+
140
+ #: RDF term for this property (i.e. the predicate URI).
141
+ self.uri = URIRef(cast(str, data.get("rdf", NS[self.qname])))
142
+
143
+ def generate(self, model: "Model") -> None:
144
+ """Setup method used when loading the model in order to build out the reverse
145
+ links of the property."""
146
+ model.properties.add(self)
147
+
148
+ if self.type == registry.entity:
149
+ if self.range is None and self._range is not None:
150
+ self.range = model.get(self._range)
151
+
152
+ if self.reverse is None and self.range and self._reverse:
153
+ if not is_mapping(self._reverse):
154
+ raise InvalidModel("Invalid reverse: %s" % self)
155
+ self.reverse = self.range._add_reverse(model, self._reverse, self)
45
156
 
46
157
  @property
47
- def label(self):
158
+ def label(self) -> str:
159
+ """User-facing title for this property."""
48
160
  return gettext(self._label)
49
161
 
50
162
  @property
51
- def description(self):
163
+ def description(self) -> str:
164
+ """A longer description of the semantics of this property."""
52
165
  return gettext(self._description)
53
166
 
54
- def validate(self, data):
167
+ def specificity(self, value: str) -> float:
168
+ """Return a measure of how precise the given value is."""
169
+ if not self.matchable:
170
+ return 0.0
171
+ return self.type.specificity(value)
172
+
173
+ def validate(self, data: List[Any]) -> Optional[str]:
55
174
  """Validate that the data should be stored.
56
175
 
57
176
  Since the types system doesn't really have validation, this currently
58
177
  tries to normalize the value to see if it passes strict parsing.
59
178
  """
60
179
  values = []
61
- for val in ensure_list(data):
62
- if is_mapping(val):
63
- val = val.get('id')
180
+ for val in data:
181
+ if self.stub:
182
+ return gettext("Property cannot be written")
183
+ val = get_entity_id(val)
184
+ if val is None:
185
+ continue
64
186
  if not self.type.validate(val):
65
- return gettext('Invalid value')
187
+ return gettext("Invalid value")
66
188
  if val is not None:
67
189
  values.append(val)
68
- if self.required and not len(values):
69
- return gettext('Required')
70
-
71
- def __eq__(self, other):
72
- return self.qname == other.qname
73
-
74
- def __hash__(self):
75
- return hash(self.qname)
76
-
77
- def to_dict(self):
78
- data = {
79
- 'name': self.name,
80
- 'qname': self.qname,
81
- 'label': self.label,
82
- 'description': self.description,
83
- 'caption': self.caption,
84
- 'uri': str(self.uri),
85
- 'type': self._type
190
+ return None
191
+
192
+ def __eq__(self, other: Any) -> bool:
193
+ return self._hash == hash(other)
194
+
195
+ def __hash__(self) -> int:
196
+ return self._hash
197
+
198
+ def to_dict(self) -> PropertyToDict:
199
+ """Return property metadata in a serializable form."""
200
+ data: PropertyToDict = {
201
+ "name": self.name,
202
+ "qname": self.qname,
203
+ "label": self.label,
204
+ "type": self.type.name,
205
+ "maxLength": self.max_length,
86
206
  }
87
- if self.type == registry.entity:
88
- data['stub'] = self.stub
89
- if self.range:
90
- data['schema'] = self.range.name
91
- if self.reverse:
92
- data['reverse'] = self.reverse.name
207
+ if self.description:
208
+ data["description"] = self.description
209
+ if self.stub:
210
+ data["stub"] = True
211
+ if self.matchable:
212
+ data["matchable"] = True
213
+ if self.hidden:
214
+ data["hidden"] = True
215
+ if self.deprecated:
216
+ data["deprecated"] = True
217
+ if self.range is not None:
218
+ data["range"] = self.range.name
219
+ if self.reverse is not None:
220
+ data["reverse"] = self.reverse.name
221
+ if self.format is not None:
222
+ data["format"] = self.format
93
223
  return data
94
224
 
95
- def __repr__(self):
96
- return '<Property(%r, %r)>' % (self.schema, self.name)
225
+ def __repr__(self) -> str:
226
+ return "<Property(%r)>" % self.qname
227
+
228
+ def __str__(self) -> str:
229
+ return self.qname