followthemoney 4.2.1__py3-none-any.whl → 4.3.4__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 (38) hide show
  1. followthemoney/__init__.py +1 -1
  2. followthemoney/compare.py +19 -0
  3. followthemoney/entity.py +15 -15
  4. followthemoney/helpers.py +13 -14
  5. followthemoney/model.py +2 -0
  6. followthemoney/property.py +23 -4
  7. followthemoney/proxy.py +8 -9
  8. followthemoney/schema/Company.yaml +9 -4
  9. followthemoney/schema/LegalEntity.yaml +18 -4
  10. followthemoney/schema/Organization.yaml +4 -0
  11. followthemoney/schema/Person.yaml +12 -0
  12. followthemoney/schema/PublicBody.yaml +3 -0
  13. followthemoney/schema/Thing.yaml +3 -2
  14. followthemoney/schema.py +16 -2
  15. followthemoney/statement/entity.py +39 -12
  16. followthemoney/statement/util.py +2 -2
  17. followthemoney/types/address.py +6 -4
  18. followthemoney/types/checksum.py +3 -3
  19. followthemoney/types/country.py +3 -3
  20. followthemoney/types/date.py +3 -3
  21. followthemoney/types/entity.py +3 -3
  22. followthemoney/types/gender.py +6 -6
  23. followthemoney/types/identifier.py +8 -8
  24. followthemoney/types/ip.py +3 -3
  25. followthemoney/types/json.py +2 -2
  26. followthemoney/types/language.py +3 -3
  27. followthemoney/types/mimetype.py +3 -3
  28. followthemoney/types/name.py +3 -3
  29. followthemoney/types/number.py +2 -2
  30. followthemoney/types/phone.py +3 -3
  31. followthemoney/types/string.py +2 -2
  32. followthemoney/types/topic.py +6 -3
  33. followthemoney/types/url.py +3 -3
  34. {followthemoney-4.2.1.dist-info → followthemoney-4.3.4.dist-info}/METADATA +3 -3
  35. {followthemoney-4.2.1.dist-info → followthemoney-4.3.4.dist-info}/RECORD +38 -38
  36. {followthemoney-4.2.1.dist-info → followthemoney-4.3.4.dist-info}/WHEEL +1 -1
  37. {followthemoney-4.2.1.dist-info → followthemoney-4.3.4.dist-info}/entry_points.txt +0 -0
  38. {followthemoney-4.2.1.dist-info → followthemoney-4.3.4.dist-info}/licenses/LICENSE +0 -0
@@ -9,7 +9,7 @@ from followthemoney.statement import Statement, StatementEntity, SE
9
9
  from followthemoney.dataset import Dataset, DefaultDataset, DS
10
10
  from followthemoney.util import set_model_locale
11
11
 
12
- __version__ = "4.2.1"
12
+ __version__ = "4.3.4"
13
13
 
14
14
  # Data model singleton
15
15
  model = Model.instance()
followthemoney/compare.py CHANGED
@@ -71,12 +71,31 @@ def _compare(scores: Scores, weights: Weights, n_std: int = 1) -> float:
71
71
  return 1.0 / (1.0 + math.exp(-prob))
72
72
 
73
73
 
74
+ def entity_is_same(left: EntityProxy, right: EntityProxy) -> bool:
75
+ """Check if two entities are the same apart from their ID."""
76
+ if left.schema != right.schema:
77
+ return False
78
+
79
+ props = set(left.properties.keys()).union(right.properties.keys())
80
+ if 0 == len(props):
81
+ return False
82
+
83
+ for prop in props:
84
+ left_vals = sorted(left.get(prop))
85
+ right_vals = sorted(right.get(prop))
86
+ if left_vals != right_vals:
87
+ return False
88
+ return True
89
+
90
+
74
91
  def compare(
75
92
  left: EntityProxy,
76
93
  right: EntityProxy,
77
94
  weights: Weights = COMPARE_WEIGHTS,
78
95
  ) -> float:
79
96
  """Compare two entities and return a match score."""
97
+ if entity_is_same(left, right):
98
+ return 1.0
80
99
  scores = compare_scores(left, right)
81
100
  return _compare(scores, weights)
82
101
 
followthemoney/entity.py CHANGED
@@ -42,25 +42,28 @@ class ValueEntity(EntityProxy):
42
42
  key_prefix: Optional[str] = None,
43
43
  cleaned: bool = True,
44
44
  ):
45
+ self._caption: Optional[str] = data.pop("caption", None)
46
+ self.datasets: Set[str] = set(data.pop("datasets", []))
47
+ self.referents: Set[str] = set(data.pop("referents", []))
48
+ self.first_seen: Optional[str] = data.pop("first_seen", None)
49
+ self.last_seen: Optional[str] = data.pop("last_seen", None)
50
+ self.last_change: Optional[str] = data.pop("last_change", None)
45
51
  super().__init__(schema, data, key_prefix=key_prefix, cleaned=cleaned)
46
- self._caption: Optional[str] = data.get("caption")
47
- self.datasets: Set[str] = set(data.get("datasets", []))
48
- self.referents: Set[str] = set(data.get("referents", []))
49
- self.first_seen: Optional[str] = data.get("first_seen")
50
- self.last_seen: Optional[str] = data.get("last_seen")
51
- self.last_change: Optional[str] = data.get("last_change")
52
52
 
53
53
  # add data from statement dict if present.
54
54
  # this updates the dataset and referents set
55
55
  for stmt_data in data.pop("statements", []):
56
56
  stmt = Statement.from_dict(stmt_data)
57
+ prop = schema.get(stmt.prop)
58
+ if prop is None:
59
+ continue
57
60
  self.datasets.add(stmt.dataset)
58
61
  if stmt.schema != self.schema.name:
59
62
  self.schema = schema.model.common_schema(self.schema, stmt.schema)
60
63
  if stmt.entity_id != self.id:
61
64
  self.referents.add(stmt.entity_id)
62
65
  if stmt.prop != BASE_ID:
63
- self.add(stmt.prop, stmt.value)
66
+ self.unsafe_add(prop, stmt.value, cleaned=cleaned)
64
67
 
65
68
  def merge(self: VE, other: EntityProxy) -> VE:
66
69
  merged = super().merge(other)
@@ -79,14 +82,11 @@ class ValueEntity(EntityProxy):
79
82
  return merged
80
83
 
81
84
  def to_dict(self) -> Dict[str, Any]:
82
- data: Dict[str, Any] = {
83
- "id": self.id,
84
- "caption": self._caption or self.caption,
85
- "schema": self.schema.name,
86
- "properties": self.properties,
87
- "referents": list(self.referents),
88
- "datasets": list(self.datasets),
89
- }
85
+ data = super().to_dict()
86
+ data["referents"] = list(self.referents)
87
+ data["datasets"] = list(self.datasets)
88
+ if self._caption is not None:
89
+ data["caption"] = self._caption
90
90
  if self.first_seen is not None:
91
91
  data["first_seen"] = self.first_seen
92
92
  if self.last_seen is not None:
followthemoney/helpers.py CHANGED
@@ -13,8 +13,7 @@ from itertools import product
13
13
  from datetime import datetime, timedelta
14
14
 
15
15
  from followthemoney.types import registry
16
- from followthemoney.proxy import E
17
- from followthemoney.util import join_text
16
+ from followthemoney.proxy import E, EntityProxy
18
17
 
19
18
  PROV_MIN_DATES = ("createdAt", "authoredAt", "publishedAt")
20
19
  PROV_MAX_DATES = ("modifiedAt", "retrievedAt")
@@ -47,7 +46,7 @@ def simplify_provenance(proxy: E) -> E:
47
46
 
48
47
 
49
48
  def entity_filename(
50
- proxy: E, base_name: Optional[str] = None, extension: Optional[str] = None
49
+ proxy: EntityProxy, base_name: Optional[str] = None, extension: Optional[str] = None
51
50
  ) -> Optional[str]:
52
51
  """Derive a safe filename for the given entity."""
53
52
  if proxy.schema.is_a("Document"):
@@ -85,7 +84,7 @@ def name_entity(entity: E) -> E:
85
84
 
86
85
 
87
86
  def check_person_cutoff(
88
- entity: E,
87
+ entity: EntityProxy,
89
88
  death_cutoff: datetime = datetime(2000, 1, 1),
90
89
  birth_cutoff: Optional[datetime] = None,
91
90
  ) -> bool:
@@ -153,17 +152,17 @@ def combine_names(entity: E) -> E:
153
152
  This is of course impossible to do culturally correctly for the whole planet at
154
153
  once, so it should be mostly used for internal-facing (e.g. matching) processes."""
155
154
  if entity.schema.is_a("Person"):
156
- first_names = entity.get("firstName")
157
- second_names = entity.get("secondName") + [""]
158
- middle_names = entity.get("middleName") + [""]
159
- father_names = entity.get("fatherName") + [""]
160
- mother_names = entity.get("motherName") + [""]
161
155
  last_names = entity.get("lastName")
162
- for (first, second, middle, father, mother, last) in product(
163
- first_names, second_names, middle_names, father_names, mother_names, last_names
164
- ):
165
- name = squash_spaces(" ".join([first, second, middle, father, mother, last]))
166
- if name is not None:
156
+ names_seq = [entity.get("firstName")]
157
+ names_seq.append(entity.get("secondName"))
158
+ names_seq.append(entity.get("middleName"))
159
+ names_seq.append(entity.get("fatherName"))
160
+ names_seq.append(entity.get("motherName"))
161
+ names_seq.append(last_names)
162
+ names_seq = [n for n in names_seq if len(n)]
163
+ for pairing in product(*names_seq):
164
+ name = squash_spaces(" ".join(pairing))
165
+ if len(name):
167
166
  entity.add("alias", name)
168
167
 
169
168
  # If no first name is given, at least add the last name:
followthemoney/model.py CHANGED
@@ -9,6 +9,7 @@ from followthemoney.types.common import PropertyType, PropertyTypeToDict
9
9
  from followthemoney.schema import Schema, SchemaToDict
10
10
  from followthemoney.property import Property
11
11
  from followthemoney.exc import InvalidModel, InvalidData
12
+ from followthemoney.util import const
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from followthemoney.proxy import EntityProxy
@@ -72,6 +73,7 @@ class Model(object):
72
73
  if not isinstance(data, dict):
73
74
  raise InvalidModel("Model file is not a mapping: %s" % filepath)
74
75
  for name, config in data.items():
76
+ name = const(name)
75
77
  self.schemata[name] = Schema(self, name, config)
76
78
 
77
79
  def get(self, name: Union[str, Schema]) -> Optional[Schema]:
@@ -1,8 +1,9 @@
1
1
  import re
2
2
  from banal import is_mapping, as_bool
3
+ from rigour.ids import get_identifier_format
3
4
  from typing import TYPE_CHECKING, Any, List, Optional, TypedDict
4
5
 
5
- from followthemoney.exc import InvalidModel
6
+ from followthemoney.exc import InvalidData, InvalidModel
6
7
  from followthemoney.types import registry
7
8
  from followthemoney.util import gettext, get_entity_id, const
8
9
 
@@ -86,17 +87,16 @@ class Property:
86
87
  self.schema = schema
87
88
 
88
89
  #: Machine-readable name for this property.
89
- self.name = const(name)
90
+ self.name = name
90
91
  if not check_property_name(self.name):
91
92
  raise InvalidModel("Invalid name: %s" % self.name)
92
93
 
93
94
  #: Qualified property name, which also includes the schema name.
94
95
  self.qname = const("%s:%s" % (schema.name, self.name))
95
96
 
96
- self._hash = hash("<Property(%r)>" % self.qname)
97
-
98
97
  self._label = data.get("label", name)
99
98
  self._description = data.get("description")
99
+ self._hash = hash("<Property(%r)>" % self.qname)
100
100
 
101
101
  #: This property is deprecated and should not be used.
102
102
  self.deprecated = as_bool(data.get("deprecated", False))
@@ -157,6 +157,13 @@ class Property:
157
157
  raise InvalidModel("Invalid reverse: %s" % self)
158
158
  self.reverse = self.range._add_reverse(model, self._reverse, self)
159
159
 
160
+ if self.type == registry.identifier and self.format is not None:
161
+ format_ = get_identifier_format(self.format)
162
+ if format_ is None or format_.NAME != self.format:
163
+ raise InvalidModel("Invalid identifier format: %s" % self.format)
164
+ # Internalize the string:
165
+ self.format = format_.NAME
166
+
160
167
  @property
161
168
  def label(self) -> str:
162
169
  """User-facing title for this property."""
@@ -229,6 +236,18 @@ class Property:
229
236
  data["format"] = self.format
230
237
  return data
231
238
 
239
+ def __reduce__(self) -> Any:
240
+ return (self._reconstruct, (self.qname,))
241
+
242
+ @classmethod
243
+ def _reconstruct(cls, qname: str) -> "Property":
244
+ from followthemoney.model import Model
245
+
246
+ prop = Model.instance().get_qname(qname)
247
+ if prop is None:
248
+ raise InvalidData("Unknown property: %r" % qname)
249
+ return prop
250
+
232
251
  def __repr__(self) -> str:
233
252
  return "<Property(%r)>" % self.qname
234
253
 
followthemoney/proxy.py CHANGED
@@ -323,7 +323,7 @@ class EntityProxy(object):
323
323
  @property
324
324
  def countries(self) -> List[str]:
325
325
  """Get the set of all country-type values set of the entity."""
326
- return self.get_type_values(registry.country)
326
+ return self.get_type_values(registry.country, matchable=True)
327
327
 
328
328
  @property
329
329
  def temporal_start(self) -> Optional[Tuple[Property, str]]:
@@ -386,6 +386,8 @@ class EntityProxy(object):
386
386
  countries = set(self.countries)
387
387
  if not len(countries):
388
388
  for prop, value in self.itervalues():
389
+ if not prop.matchable:
390
+ continue
389
391
  hint = prop.type.country_hint(value)
390
392
  if hint is not None:
391
393
  countries.add(hint)
@@ -401,13 +403,10 @@ class EntityProxy(object):
401
403
  schema and any contextual values that were handed in initially. The resulting
402
404
  dictionary can be used to make a new proxy, and it is commonly written to disk
403
405
  or a database."""
404
- data = dict(self.context)
405
- extra = {
406
- "id": self.id,
407
- "schema": self.schema.name,
408
- "properties": self.properties,
409
- }
410
- data.update(extra)
406
+ data: Dict[str, Any] = dict(self.context)
407
+ data["id"] = self.id
408
+ data["schema"] = self.schema.name
409
+ data["properties"] = self.properties
411
410
  return data
412
411
 
413
412
  def to_full_dict(self, matchable: bool = False) -> Dict[str, Any]:
@@ -460,7 +459,7 @@ class EntityProxy(object):
460
459
  return self._size
461
460
 
462
461
  def __hash__(self) -> int:
463
- if not self.id:
462
+ if self.id is None:
464
463
  raise RuntimeError("Cannot hash entity without an ID")
465
464
  return hash(self.id)
466
465
 
@@ -18,6 +18,10 @@ Company:
18
18
  - name
19
19
  caption:
20
20
  - name
21
+ - alias
22
+ - weakAlias
23
+ - previousName
24
+ - registrationNumber
21
25
  properties:
22
26
  jurisdiction:
23
27
  label: Jurisdiction
@@ -58,20 +62,20 @@ Company:
58
62
  type: identifier
59
63
  caemCode:
60
64
  label: "COD CAEM"
61
- description: "(RO) What kind of activity a legal entity is allowed to develop"
65
+ description: "Romanian classifier used to identify the types of economic activities that a business can provide in Romania"
62
66
  matchable: false
63
67
  kppCode:
64
68
  label: "KPP"
65
- description: "(RU, КПП) in addition to INN for orgs; reason for registration at FNS"
69
+ description: "Russian code issued by the tax authority, identifies the reason for registration at the Federal Tax Service (Russian: КПП). A company may have multiple KPP codes (e.g. for different branches), and the codes are not unique across companies."
66
70
  type: identifier
67
71
  matchable: false
68
72
  okvedCode:
69
73
  label: "OKVED(2) Classifier"
70
- description: "(RU, ОКВЭД) Economical activity classifier. OKVED2 is the same but newer"
74
+ description: "Russian classifier that that categorizes businesses by their (primary and secondary) economic activities (Russian: ОКВЭД)"
71
75
  matchable: false
72
76
  okopfCode:
73
77
  label: "OKOPF"
74
- description: "(RU, ОКОПФ) What kind of business entity"
78
+ description: "Russian classifier that that categorizes different types of legal entities in Russia based on their organizational and legal structure (Russian: ОКОПФ)"
75
79
  matchable: false
76
80
  fnsCode:
77
81
  label: "Federal tax service code"
@@ -84,6 +88,7 @@ Company:
84
88
  bikCode:
85
89
  label: "BIK"
86
90
  description: "Russian bank account code"
91
+ type: identifier
87
92
  pfrNumber:
88
93
  label: "PFR Number"
89
94
  description: "(RU, ПФР) Pension Fund Registration number. AAA-BBB-CCCCCC, where AAA is organisation region, BBB is district, CCCCCC number at a specific branch"
@@ -17,6 +17,9 @@ LegalEntity:
17
17
  - name
18
18
  caption:
19
19
  - name
20
+ - alias
21
+ - weakAlias
22
+ - previousName
20
23
  - email
21
24
  - phone
22
25
  - registrationNumber
@@ -74,7 +77,7 @@ LegalEntity:
74
77
  idNumber:
75
78
  label: ID Number
76
79
  type: identifier
77
- description: "ID number of any applicable ID"
80
+ description: "ID of any applicable personal identification document. Used mainly for people and their national ID cards."
78
81
  taxNumber:
79
82
  label: Tax Number
80
83
  type: identifier
@@ -102,6 +105,17 @@ LegalEntity:
102
105
  bvdId:
103
106
  label: Bureau van Dijk ID
104
107
  type: identifier
108
+ sayariId:
109
+ label: Sayari Entity ID
110
+ type: identifier
111
+ brightQueryId:
112
+ label: BrightQuery ID
113
+ type: identifier
114
+ brightQueryOrgId:
115
+ label: BrightQuery Organization ID
116
+ type: identifier
117
+ hidden: true
118
+ matchable: false
105
119
  uscCode:
106
120
  # cf. https://en.wikipedia.org/wiki/Unified_Social_Credit_Identifier
107
121
  label: "USCC"
@@ -118,13 +132,13 @@ LegalEntity:
118
132
  matchable: false
119
133
  innCode:
120
134
  label: "INN"
121
- description: "Russian company ID"
135
+ description: "Russian tax identification number (Russian: ИНН). Issued to businesses and individuals in Russia"
122
136
  type: identifier
123
137
  format: inn
124
138
  maxLength: 32
125
139
  ogrnCode:
126
140
  label: "OGRN"
127
- description: "Major State Registration Number"
141
+ description: "Identification number used in Russia's Unified State Register of Legal Entities (EGRUL) (Russian: ОГРН)"
128
142
  type: identifier
129
143
  format: ogrn
130
144
  maxLength: 32
@@ -148,7 +162,7 @@ LegalEntity:
148
162
  maxLength: 32
149
163
  npiCode:
150
164
  label: "NPI"
151
- description: "National Provider Identifier"
165
+ description: "National Provider Identifier, issued to health care providers in the United States"
152
166
  type: identifier
153
167
  format: npi
154
168
  maxLength: 16
@@ -17,6 +17,10 @@ Organization:
17
17
  - name
18
18
  caption:
19
19
  - name
20
+ - alias
21
+ - weakAlias
22
+ - previousName
23
+ - registrationNumber
20
24
  properties:
21
25
  cageCode:
22
26
  label: CAGE
@@ -14,6 +14,9 @@ Person:
14
14
  - name
15
15
  caption:
16
16
  - name
17
+ - alias
18
+ - weakAlias
19
+ - previousName
17
20
  - lastName
18
21
  - email
19
22
  - phone
@@ -31,16 +34,23 @@ Person:
31
34
  # too many false positives.
32
35
  firstName:
33
36
  label: First name
37
+ description: "The part of a name that indicates the person, also often called given name or forename"
34
38
  secondName:
35
39
  label: Second name
40
+ description: "Deprecated, use one of the other more specific name properties instead."
41
+ deprecated: true
36
42
  middleName:
37
43
  label: Middle name
44
+ description: "The part of name written between a person's given name and family name. Often abbreviated as a middle initial."
38
45
  fatherName:
39
46
  label: Patronymic
47
+ description: "The part of a name based on the given name of one's father"
40
48
  motherName:
41
49
  label: Matronymic
50
+ description: "The part of a name based on the given name of one's mother"
42
51
  lastName:
43
52
  label: Last name
53
+ description: "The part of a name that indicates one's family, also often called surname or family name"
44
54
  nameSuffix:
45
55
  label: Name suffix
46
56
  birthDate:
@@ -92,6 +102,8 @@ Person:
92
102
  label: Religion
93
103
  political:
94
104
  label: Political association
105
+ profession:
106
+ label: Profession
95
107
  education:
96
108
  label: Education
97
109
  spokenLanguage:
@@ -13,5 +13,8 @@ PublicBody:
13
13
  - status
14
14
  caption:
15
15
  - name
16
+ - alias
17
+ - weakAlias
18
+ - previousName
16
19
  required:
17
20
  - name
@@ -24,7 +24,7 @@ Thing:
24
24
  label: Country
25
25
  type: country
26
26
  alias:
27
- label: Other name
27
+ label: Alias
28
28
  type: name
29
29
  previousName:
30
30
  label: Previous name
@@ -32,6 +32,7 @@ Thing:
32
32
  weakAlias:
33
33
  label: Weak alias
34
34
  type: name
35
+ description: "A relatively broad or generic alias that should not be used for matching in screening systems. It may still may be useful for identification purposes, particularly in confirming a possible match triggered by other identifier information."
35
36
  matchable: false
36
37
  sourceUrl:
37
38
  label: Source link
@@ -55,7 +56,7 @@ Thing:
55
56
  wikidataId:
56
57
  label: Wikidata ID
57
58
  type: identifier
58
- format: qid
59
+ format: wikidata
59
60
  maxLength: 32
60
61
  keywords:
61
62
  label: Keywords
followthemoney/schema.py CHANGED
@@ -106,7 +106,7 @@ class Schema:
106
106
 
107
107
  def __init__(self, model: "Model", name: str, data: SchemaSpec) -> None:
108
108
  #: Machine-readable name of the schema, used for identification.
109
- self.name = const(name)
109
+ self.name = name
110
110
  self.model = model
111
111
  self._label = data.get("label", name)
112
112
  self._plural = data.get("plural", self.label)
@@ -191,6 +191,7 @@ class Schema:
191
191
  #: inherited from parent schemata.
192
192
  self.properties: Dict[str, Property] = {}
193
193
  for pname, prop in data.get("properties", {}).items():
194
+ pname = const(pname)
194
195
  self.properties[pname] = Property(self, pname, prop)
195
196
 
196
197
  def generate(self, model: "Model") -> None:
@@ -264,6 +265,7 @@ class Schema:
264
265
  name = data.get("name")
265
266
  if name is None:
266
267
  raise InvalidModel("Unnamed reverse: %s" % other)
268
+ name = const(name)
267
269
 
268
270
  prop = self.get(name)
269
271
  if prop is None:
@@ -272,7 +274,7 @@ class Schema:
272
274
  "type": registry.entity.name,
273
275
  "reverse": {"name": other.name},
274
276
  "range": other.schema.name,
275
- "hidden": data.get("hidden", other.hidden),
277
+ "hidden": as_bool(data.get("hidden", other.hidden)),
276
278
  }
277
279
  prop = Property(self, name, spec)
278
280
  prop.stub = True
@@ -466,6 +468,18 @@ class Schema:
466
468
  data["properties"] = properties
467
469
  return data
468
470
 
471
+ def __reduce__(self) -> Any:
472
+ return (self._reconstruct, (self.name,))
473
+
474
+ @classmethod
475
+ def _reconstruct(cls, name: str) -> "Schema":
476
+ from followthemoney.model import Model
477
+
478
+ schema = Model.instance().get(name)
479
+ if schema is None:
480
+ raise InvalidData("Unknown schema: %r" % name)
481
+ return schema
482
+
469
483
  def __eq__(self, other: Any) -> bool:
470
484
  """Compare two schemata (via hash)."""
471
485
  try:
@@ -7,6 +7,7 @@ from rigour.names.pick import pick_lang_name
7
7
 
8
8
  from followthemoney.model import Model
9
9
  from followthemoney.exc import InvalidData
10
+ from followthemoney.schema import Schema
10
11
  from followthemoney.types.common import PropertyType
11
12
  from followthemoney.property import Property
12
13
  from followthemoney.util import gettext
@@ -361,11 +362,11 @@ class StatementEntity(EntityProxy):
361
362
  name = pick_lang_name(values)
362
363
  if name is not None:
363
364
  self._caption = name
364
- break
365
- else:
366
- for stmt in sorted(stmts):
367
- self._caption = stmt.value
368
- break
365
+ return self._caption
366
+
367
+ for stmt in sorted(stmts):
368
+ self._caption = stmt.value
369
+ return self._caption
369
370
  if self._caption is None:
370
371
  self._caption = self.schema.label
371
372
  return self._caption
@@ -477,12 +478,38 @@ class StatementEntity(EntityProxy):
477
478
  dataset: Dataset,
478
479
  statements: Iterable[Statement],
479
480
  ) -> SE:
480
- obj: Optional[SE] = None
481
+ model = Model.instance()
482
+ canonical_id: Optional[str] = None
483
+ schemata: Set[str] = set()
484
+ first_seens: Set[str] = set()
485
+ props: Dict[str, Set[Statement]] = {}
481
486
  for stmt in statements:
482
- if obj is None:
483
- data = {"schema": stmt.schema, "id": stmt.canonical_id}
484
- obj = cls(dataset, data)
485
- obj.add_statement(stmt)
486
- if obj is None:
487
- raise ValueError("No statements given!")
487
+ schemata.add(stmt.schema)
488
+ canonical_id = stmt.canonical_id or canonical_id or stmt.entity_id
489
+ if stmt.prop == BASE_ID:
490
+ if stmt.first_seen is not None:
491
+ first_seens.add(stmt.first_seen)
492
+ else:
493
+ if stmt.prop not in props:
494
+ props[stmt.prop] = set()
495
+ props[stmt.prop].add(stmt)
496
+
497
+ schema: Optional[Schema] = None
498
+ for name in schemata:
499
+ if schema is None:
500
+ schema = model.get(name)
501
+ elif schema.name != name:
502
+ try:
503
+ schema = model.common_schema(schema, name)
504
+ except InvalidData as exc:
505
+ raise InvalidData(f"{canonical_id}: {exc}") from exc
506
+
507
+ if schema is None:
508
+ err = "No valid schema for entity: %s %r" % (canonical_id, schemata)
509
+ raise InvalidData(err)
510
+
511
+ data = {"schema": schema, "id": canonical_id}
512
+ obj = cls(dataset, data)
513
+ obj.last_change = max(first_seens, default=None)
514
+ obj._statements = {p: s for p, s in props.items()}
488
515
  return obj
@@ -1,8 +1,8 @@
1
- import sys
2
1
  from functools import cache
3
2
  from typing import Tuple
4
3
 
5
4
  from followthemoney.model import Model
5
+ from followthemoney.util import const
6
6
 
7
7
  BASE_ID = "id"
8
8
 
@@ -28,4 +28,4 @@ def get_prop_type(schema: str, prop: str) -> str:
28
28
  def unpack_prop(id: str) -> Tuple[str, str, str]:
29
29
  schema, prop = id.split(":", 1)
30
30
  prop_type = get_prop_type(schema, prop)
31
- return sys.intern(schema), prop_type, sys.intern(prop)
31
+ return const(schema), prop_type, const(prop)
@@ -6,7 +6,7 @@ from rigour.text.distance import levenshtein_similarity
6
6
 
7
7
  from followthemoney.types.common import PropertyType
8
8
  from followthemoney.util import defer as _
9
- from followthemoney.util import dampen, const
9
+ from followthemoney.util import dampen
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from followthemoney.proxy import EntityProxy
@@ -20,8 +20,8 @@ class AddressType(PropertyType):
20
20
 
21
21
  LINE_BREAKS = re.compile(r"(\r\n|\n|<BR/>|<BR>|\t|ESQ\.,|ESQ,|;)")
22
22
  COMMATA = re.compile(r"(,\s?[,\.])")
23
- name = const("address")
24
- group = const("addresses")
23
+ name = "address"
24
+ group = "addresses"
25
25
  label = _("Address")
26
26
  plural = _("Addresses")
27
27
  matchable = True
@@ -47,7 +47,9 @@ class AddressType(PropertyType):
47
47
  right_norm = normalize_address(right)
48
48
  if left_norm is None or right_norm is None:
49
49
  return 0.0
50
- return levenshtein_similarity(left_norm, right_norm, max_edits=3)
50
+ base_len = min(len(left_norm), len(right_norm))
51
+ max_edits = int(base_len * 0.33)
52
+ return levenshtein_similarity(left_norm, right_norm, max_edits=max_edits)
51
53
 
52
54
  def _specificity(self, value: str) -> float:
53
55
  return dampen(10, 60, value)
@@ -1,5 +1,5 @@
1
1
  from followthemoney.types.common import PropertyType
2
- from followthemoney.util import const, defer as _
2
+ from followthemoney.util import defer as _
3
3
 
4
4
 
5
5
  class ChecksumType(PropertyType):
@@ -12,8 +12,8 @@ class ChecksumType(PropertyType):
12
12
  of this type are scrubbed when submitted via the normal API. Checksums can only
13
13
  be defined by uploading a document to be ingested."""
14
14
 
15
- name = const("checksum")
16
- group = const("checksums")
15
+ name = "checksum"
16
+ group = "checksums"
17
17
  label = _("Checksum")
18
18
  plural = _("Checksums")
19
19
  matchable = True
@@ -3,7 +3,7 @@ from babel.core import Locale
3
3
  from rigour.territories import get_ftm_countries, lookup_territory
4
4
 
5
5
  from followthemoney.types.common import EnumType, EnumValues
6
- from followthemoney.util import const, defer as _
6
+ from followthemoney.util import defer as _
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from followthemoney.proxy import EntityProxy
@@ -15,8 +15,8 @@ class CountryType(EnumType):
15
15
  a number of unusual and controversial designations (e.g. the Soviet Union,
16
16
  Transnistria, Somaliland, Kosovo)."""
17
17
 
18
- name = const("country")
19
- group = const("countries")
18
+ name = "country"
19
+ group = "countries"
20
20
  label = _("Country")
21
21
  plural = _("Countries")
22
22
  matchable = True
@@ -5,7 +5,7 @@ from prefixdate import parse, parse_format, Precision
5
5
 
6
6
  from followthemoney.types.common import PropertyType
7
7
  from followthemoney.util import defer as _
8
- from followthemoney.util import dampen, const
8
+ from followthemoney.util import dampen
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from followthemoney.proxy import EntityProxy
@@ -20,8 +20,8 @@ class DateType(PropertyType):
20
20
  The timezone is always expected to be UTC and cannot be specified otherwise. There is
21
21
  no support for calendar weeks (`2021-W7`) and date ranges (`2021-2024`)."""
22
22
 
23
- name = const("date")
24
- group = const("dates")
23
+ name = "date"
24
+ group = "dates"
25
25
  label = _("Date")
26
26
  plural = _("Dates")
27
27
  matchable = True
@@ -4,7 +4,7 @@ from typing import Any, Optional, TYPE_CHECKING
4
4
  from followthemoney.types.common import PropertyType
5
5
  from followthemoney.value import Value
6
6
  from followthemoney.util import ENTITY_ID_LEN, get_entity_id, sanitize_text
7
- from followthemoney.util import const, gettext, defer as _
7
+ from followthemoney.util import gettext, defer as _
8
8
  from followthemoney.exc import InvalidData
9
9
 
10
10
  if TYPE_CHECKING:
@@ -22,8 +22,8 @@ class EntityType(PropertyType):
22
22
 
23
23
  REGEX_RAW = r"^[0-9a-zA-Z]([0-9a-zA-Z\.\-]*[0-9a-zA-Z])?$"
24
24
  REGEX = re.compile(REGEX_RAW)
25
- name = const("entity")
26
- group = const("entities")
25
+ name = "entity"
26
+ group = "entities"
27
27
  label = _("Entity")
28
28
  plural = _("Entities")
29
29
  matchable = True
@@ -2,7 +2,7 @@ from typing import Optional, TYPE_CHECKING
2
2
  from babel.core import Locale
3
3
 
4
4
  from followthemoney.types.common import EnumType, EnumValues
5
- from followthemoney.util import const, gettext, defer as _
5
+ from followthemoney.util import gettext, defer as _
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from followthemoney.proxy import EntityProxy
@@ -14,9 +14,9 @@ class GenderType(EnumType):
14
14
  government databases and represent it in a way that can be used by
15
15
  structured tools. I'm not sure this justifies the simplification."""
16
16
 
17
- MALE = const("male")
18
- FEMALE = const("female")
19
- OTHER = const("other")
17
+ MALE = "male"
18
+ FEMALE = "female"
19
+ OTHER = "other"
20
20
 
21
21
  LOOKUP = {
22
22
  "m": MALE,
@@ -34,8 +34,8 @@ class GenderType(EnumType):
34
34
  "divers": OTHER,
35
35
  }
36
36
 
37
- name = const("gender")
38
- group = const("genders")
37
+ name = "gender"
38
+ group = "genders"
39
39
  label = _("Gender")
40
40
  plural = _("Genders")
41
41
  matchable = False
@@ -1,10 +1,10 @@
1
1
  import re
2
2
  from typing import Optional, TYPE_CHECKING
3
- from rigour.ids import get_identifier_format_names, get_identifier_format
3
+ from rigour.ids import get_identifier_format
4
4
 
5
5
  from followthemoney.types.common import PropertyType
6
6
  from followthemoney.util import dampen, shortest, longest
7
- from followthemoney.util import const, defer as _
7
+ from followthemoney.util import defer as _
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from followthemoney.proxy import EntityProxy
@@ -20,8 +20,8 @@ class IdentifierType(PropertyType):
20
20
  Four- or five-digit industry classifiers create more noise than value."""
21
21
 
22
22
  COMPARE_CLEAN = re.compile(r"[\W_]+")
23
- name = const("identifier")
24
- group = const("identifiers")
23
+ name = "identifier"
24
+ group = "identifiers"
25
25
  label = _("Identifier")
26
26
  plural = _("Identifiers")
27
27
  matchable = True
@@ -35,8 +35,8 @@ class IdentifierType(PropertyType):
35
35
  format: Optional[str] = None,
36
36
  proxy: Optional["EntityProxy"] = None,
37
37
  ) -> Optional[str]:
38
- if format in get_identifier_format_names():
39
- format_ = get_identifier_format(format)
38
+ format_ = get_identifier_format(format)
39
+ if format_ is not None:
40
40
  return format_.normalize(text)
41
41
  return text
42
42
 
@@ -61,7 +61,7 @@ class IdentifierType(PropertyType):
61
61
  return f"id:{value}"
62
62
 
63
63
  def caption(self, value: str, format: Optional[str] = None) -> str:
64
- if format in get_identifier_format_names():
65
- format_ = get_identifier_format(format)
64
+ format_ = get_identifier_format(format)
65
+ if format_ is not None:
66
66
  return format_.format(value)
67
67
  return value
@@ -2,7 +2,7 @@ from typing import Optional, TYPE_CHECKING
2
2
  from ipaddress import ip_address
3
3
 
4
4
  from followthemoney.types.common import PropertyType
5
- from followthemoney.util import const, defer as _
5
+ from followthemoney.util import defer as _
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from followthemoney.proxy import EntityProxy
@@ -13,8 +13,8 @@ class IpType(PropertyType):
13
13
  by the protocol versions 4 (e.g. `192.168.1.143`) and 6
14
14
  (e.g. `0:0:0:0:0:ffff:c0a8:18f`)."""
15
15
 
16
- name = const("ip")
17
- group = const("ips")
16
+ name = "ip"
17
+ group = "ips"
18
18
  label = _("IP Address")
19
19
  plural = _("IP Addresses")
20
20
  matchable = True
@@ -3,7 +3,7 @@ from typing import Any, Optional, Sequence, TYPE_CHECKING
3
3
  from banal import ensure_list
4
4
 
5
5
  from followthemoney.types.common import PropertyType
6
- from followthemoney.util import const, sanitize_text, defer as _
6
+ from followthemoney.util import sanitize_text, defer as _
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from followthemoney.proxy import EntityProxy
@@ -14,7 +14,7 @@ class JsonType(PropertyType):
14
14
  and some other edge cases. It's a really bad idea and we should try to get rid
15
15
  of JSON properties."""
16
16
 
17
- name = const("json")
17
+ name = "json"
18
18
  group = None
19
19
  label = _("Nested data")
20
20
  plural = _("Nested data")
@@ -4,7 +4,7 @@ from rigour.langs import iso_639_alpha3
4
4
 
5
5
  from followthemoney.types.common import EnumType, EnumValues
6
6
  from followthemoney.util import defer as _, gettext
7
- from followthemoney.util import const, get_env_list
7
+ from followthemoney.util import get_env_list
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from followthemoney.proxy import EntityProxy
@@ -16,8 +16,8 @@ class LanguageType(EnumType):
16
16
  for additional languages once there is a specific need for them to be
17
17
  supported."""
18
18
 
19
- name = const("language")
20
- group = const("languages")
19
+ name = "language"
20
+ group = "languages"
21
21
  label = _("Language")
22
22
  plural = _("Languages")
23
23
  matchable = False
@@ -3,7 +3,7 @@ from rigour.mime import normalize_mimetype, parse_mimetype
3
3
  from rigour.mime import DEFAULT
4
4
 
5
5
  from followthemoney.types.common import PropertyType
6
- from followthemoney.util import const, defer as _
6
+ from followthemoney.util import defer as _
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from followthemoney.proxy import EntityProxy
@@ -18,8 +18,8 @@ class MimeType(PropertyType):
18
18
  MIME type properties do not contain parameters as used in HTTP headers,
19
19
  like `charset=UTF-8`."""
20
20
 
21
- name = const("mimetype")
22
- group = const("mimetypes")
21
+ name = "mimetype"
22
+ group = "mimetypes"
23
23
  label = _("MIME-Type")
24
24
  plural = _("MIME-Types")
25
25
  matchable = False
@@ -7,7 +7,7 @@ from rigour.text.distance import levenshtein_similarity
7
7
 
8
8
  from followthemoney.types.common import PropertyType
9
9
  from followthemoney.util import dampen
10
- from followthemoney.util import const, defer as _
10
+ from followthemoney.util import defer as _
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from followthemoney.proxy import EntityProxy
@@ -21,8 +21,8 @@ class NameType(PropertyType):
21
21
  No validation rules apply, and things having multiple names must be considered
22
22
  a perfectly ordinary case."""
23
23
 
24
- name = const("name")
25
- group = const("names")
24
+ name = "name"
25
+ group = "names"
26
26
  label = _("Name")
27
27
  plural = _("Names")
28
28
  matchable = True
@@ -2,7 +2,7 @@ import re
2
2
  from typing import Optional, Tuple
3
3
 
4
4
  from followthemoney.types.common import PropertyType
5
- from followthemoney.util import const, defer as _
5
+ from followthemoney.util import defer as _
6
6
 
7
7
 
8
8
  class NumberType(PropertyType):
@@ -24,7 +24,7 @@ class NumberType(PropertyType):
24
24
  _FLOAT_FMT = "{:" + SEPARATOR + "." + str(PRECISION) + "f}"
25
25
  _INT_FMT = "{:" + SEPARATOR + "d}"
26
26
 
27
- name = const("number")
27
+ name = "number"
28
28
  label = _("Number")
29
29
  plural = _("Numbers")
30
30
  matchable = False
@@ -6,7 +6,7 @@ from phonenumbers.phonenumberutil import region_code_for_number, NumberParseExce
6
6
 
7
7
  from followthemoney.types.common import PropertyType
8
8
  from followthemoney.util import defer as _
9
- from followthemoney.util import const, dampen
9
+ from followthemoney.util import dampen
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from followthemoney.proxy import EntityProxy
@@ -29,8 +29,8 @@ class PhoneType(PropertyType):
29
29
  validation outcome from doing the two operations the other way around. Always
30
30
  define the country first."""
31
31
 
32
- name = const("phone")
33
- group = const("phones")
32
+ name = "phone"
33
+ group = "phones"
34
34
  label = _("Phone number")
35
35
  plural = _("Phone numbers")
36
36
  matchable = True
@@ -6,7 +6,7 @@ from followthemoney.util import MEGABYTE
6
6
  class StringType(PropertyType):
7
7
  """A simple string property with no additional semantics."""
8
8
 
9
- name = const("string")
9
+ name = "string"
10
10
  label = _("Label")
11
11
  plural = _("Labels")
12
12
  matchable = False
@@ -21,7 +21,7 @@ class TextType(StringType):
21
21
  string properties, it might make sense to treat properties of this type as
22
22
  full-text search material."""
23
23
 
24
- name = const("text")
24
+ name = "text"
25
25
  label = _("Text")
26
26
  plural = _("Texts")
27
27
  total_size = 30 * MEGABYTE
@@ -1,7 +1,7 @@
1
1
  from babel.core import Locale
2
2
 
3
3
  from followthemoney.types.common import EnumType, EnumValues
4
- from followthemoney.util import const, gettext, defer as _
4
+ from followthemoney.util import gettext, defer as _
5
5
 
6
6
 
7
7
  class TopicType(EnumType):
@@ -15,8 +15,8 @@ class TopicType(EnumType):
15
15
  enable queries such as _find all paths between a government procurement
16
16
  award and a politician_."""
17
17
 
18
- name = const("topic")
19
- group = const("topics")
18
+ name = "topic"
19
+ group = "topics"
20
20
  label = _("Topic")
21
21
  plural = _("Topics")
22
22
  matchable = False
@@ -86,6 +86,8 @@ class TopicType(EnumType):
86
86
  "sanction.linked": _("Sanction-linked entity"),
87
87
  "sanction.counter": _("Counter-sanctioned entity"),
88
88
  "export.control": _("Export controlled"),
89
+ # For BIS 50% rule:
90
+ "export.control.linked": _("Export control-linked"),
89
91
  "export.risk": _("Trade risk"),
90
92
  "debarment": _("Debarred entity"),
91
93
  "poi": _("Person of interest"),
@@ -103,6 +105,7 @@ class TopicType(EnumType):
103
105
  "crime",
104
106
  "debarment",
105
107
  "export.control",
108
+ "export.control.linked",
106
109
  "export.risk",
107
110
  "poi",
108
111
  "mare.detained",
@@ -2,7 +2,7 @@ from typing import Optional, TYPE_CHECKING
2
2
  from rigour.urls import clean_url, compare_urls
3
3
 
4
4
  from followthemoney.types.common import PropertyType
5
- from followthemoney.util import const, dampen, defer as _
5
+ from followthemoney.util import dampen, defer as _
6
6
 
7
7
  if TYPE_CHECKING:
8
8
  from followthemoney.proxy import EntityProxy
@@ -16,8 +16,8 @@ class UrlType(PropertyType):
16
16
  SCHEMES = ("http", "https", "ftp", "mailto")
17
17
  DEFAULT_SCHEME = "http"
18
18
 
19
- name = const("url")
20
- group = const("urls")
19
+ name = "url"
20
+ group = "urls"
21
21
  label = _("URL")
22
22
  plural = _("URLs")
23
23
  matchable = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: followthemoney
3
- Version: 4.2.1
3
+ Version: 4.3.4
4
4
  Summary: A data model for anti corruption data modeling and analysis.
5
5
  Project-URL: Documentation, https://followthemoney.tech/
6
6
  Project-URL: Repository, https://github.com/opensanctions/followthemoney.git
@@ -48,9 +48,9 @@ Requires-Dist: prefixdate<1.0.0,>=0.5.0
48
48
  Requires-Dist: pydantic<3.0.0,>=2.11.0
49
49
  Requires-Dist: pytz>=2021.1
50
50
  Requires-Dist: pyyaml<7.0.0,>=5.0.0
51
- Requires-Dist: rdflib<7.2.0,>=6.2.0
51
+ Requires-Dist: rdflib<7.5.0,>=6.2.0
52
52
  Requires-Dist: requests<3.0.0,>=2.21.0
53
- Requires-Dist: rigour<2.0.0,>=1.3.1
53
+ Requires-Dist: rigour<2.0.0,>=1.4.0
54
54
  Requires-Dist: sqlalchemy[mypy]<3.0.0,>=2.0.0
55
55
  Provides-Extra: dev
56
56
  Requires-Dist: build; extra == 'dev'
@@ -1,18 +1,18 @@
1
- followthemoney/__init__.py,sha256=X-mG5wtVuhLqrHVWmz8NnB7mBFYuMwJj_wfYkL63n4M,856
2
- followthemoney/compare.py,sha256=bZlnj2VMoe67q4Lyq_VwS1a-EJnEK1kC8prbs8jyL9E,5774
3
- followthemoney/entity.py,sha256=bBiX7hNquXemS3vYCUHKtWI_IqX43Z6i8RQDbZ7gXsg,3449
1
+ followthemoney/__init__.py,sha256=UHPYwFuppho0TsOPG7vZw6KSsIDHX_Ar7gO1Vw7dLig,856
2
+ followthemoney/compare.py,sha256=frgumsDv4Ru9UkNof62jDjKCxxpCgV1Rusfu8s20uGA,6327
3
+ followthemoney/entity.py,sha256=YB6u7BMeQX5toAe7DndZBiPtzy0BQ5CKp3ix6kHxk3Y,3499
4
4
  followthemoney/exc.py,sha256=GyMgwY4QVm87hLevDfV7gM1MJsDqfNCi_UQw7F_A8X8,858
5
5
  followthemoney/graph.py,sha256=7X1CGHGvmktS2LSZqld2iXWzG7B831eCNYyBqamqEJ8,10921
6
- followthemoney/helpers.py,sha256=EsneNJ5DZXHTPUYfXLxGESphsPwCtGiWv-71vvVBWus,7982
6
+ followthemoney/helpers.py,sha256=KCdv1XAE7KQEXBiXp52Kvuck7wMaeNVBM3uaFemcvb4,7873
7
7
  followthemoney/messages.py,sha256=zUEa9CFecU8nRafIzhN6TKCh1kEihiIyIS1qr8PxY4g,806
8
- followthemoney/model.py,sha256=bWFVNa-DhYzc8BdSXBZdG2ev6Nh9uHx6i4tin8DvEEU,7374
8
+ followthemoney/model.py,sha256=chAUGob5tXWS0o8f0X6mSFCCnI2HoHE5pXU9O5ukrpc,7447
9
9
  followthemoney/names.py,sha256=LODQqExKEHdH4z6Mmbhlm0KeKRzGcptaSWzYXZ7lONI,1120
10
10
  followthemoney/namespace.py,sha256=utggu9IGA8bhgEYom3OUB1KxkAJR_TrMNbY5MUF_db8,4536
11
11
  followthemoney/ontology.py,sha256=WWY_PYQGl5Ket4zZBuZglzQxD2Bh9UqHok6GJNNX7GA,3001
12
- followthemoney/property.py,sha256=9qZ_o2iA-1llLMJ3O2hsW7c2XhkFU1YbvVqretGYUSA,7913
13
- followthemoney/proxy.py,sha256=LD4K1oPABXMX212UZxwLu7XOHRDyVBwTlqudTUsUZRQ,19619
12
+ followthemoney/property.py,sha256=1w7p9aKLxRqFRnl3PlssqmvulSErl_0D5T2SefT3UFU,8675
13
+ followthemoney/proxy.py,sha256=xZUsT4W9sLojaSD8j6P2JQyQuOQKBWDVovm5epxvtI0,19674
14
14
  followthemoney/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- followthemoney/schema.py,sha256=WYnPE4Lego0pJHlojECEv0aO9Miw_YIvEb35HoDo4Zk,18087
15
+ followthemoney/schema.py,sha256=dwZg0eZF7yaxP9fJ5NQUKWadWZYTo9U-sVzzXZn_6do,18500
16
16
  followthemoney/util.py,sha256=LoCSp1iE6VwXjotCkBXFRppeQs55726GzOuNIu3CvRE,4409
17
17
  followthemoney/value.py,sha256=BJ4Sj5Tg2kMrslR6FjQUr96d8Kt75U7ny9NgzVGT0ZE,2335
18
18
  followthemoney/cli/__init__.py,sha256=0mmz84uhXRp2qUn3syKnDXofU3MMAAe291s7htqX0Bg,187
@@ -54,7 +54,7 @@ followthemoney/schema/Audio.yaml,sha256=Eb1rZGUEOX7XDAj_1YIN28NCBzMvkopQBNwgHt_k
54
54
  followthemoney/schema/BankAccount.yaml,sha256=60v-VD296lW1Qq7fx--CzxfPNwfCcyMV6xIl8OrSy5g,1431
55
55
  followthemoney/schema/Call.yaml,sha256=kbVCnVxucBrEplxehXHThLSJAJjy_GhWan-IeZZjr0M,980
56
56
  followthemoney/schema/CallForTenders.yaml,sha256=2IWonTzfSbrkynMoEWqv5fekUeFM_xDKpKIbRe1XDbo,3227
57
- followthemoney/schema/Company.yaml,sha256=v16tU4mQhVitC3BOc9OPbubCxPgf1c-Iudnyq6IF0ys,3057
57
+ followthemoney/schema/Company.yaml,sha256=1P_JA2LIIQtzP_Y8FYM-a47VNSWneX8QjdmGD1DFpWQ,3527
58
58
  followthemoney/schema/Contract.yaml,sha256=aSPB64T1h-0nuLDv6krasUvvoPZgo6sWUbv60c3vmzI,1541
59
59
  followthemoney/schema/ContractAward.yaml,sha256=b2spaZHYCaP1yR1RCsrI7mUjk-fAF7BUE3dc8Vl3cUQ,1689
60
60
  followthemoney/schema/CourtCase.yaml,sha256=lcovnY0Ne_xcggvkqfCW_RHvsRKo8kFTCPCyovAXRtI,599
@@ -75,26 +75,26 @@ followthemoney/schema/Identification.yaml,sha256=6txjZs6-3Kn94c3G4tDeDt9Jb4FW55-
75
75
  followthemoney/schema/Image.yaml,sha256=wuznboWECGiV96_GQiXq1-oKNoxO8zKisR4xyusnEn8,394
76
76
  followthemoney/schema/Interest.yaml,sha256=VUrehmsN1WgtS1oAa5jn_JGtSkZGGYLGNahp-R5JhOQ,282
77
77
  followthemoney/schema/Interval.yaml,sha256=8YJQ51GI-GxvbjYs3uC593kQtCepWW_7ZiNnlbPm2aM,2084
78
- followthemoney/schema/LegalEntity.yaml,sha256=u6GOemHj1vnL_6dcC3XyLaP72JQrNecXlbocXxJo5AI,4502
78
+ followthemoney/schema/LegalEntity.yaml,sha256=Yy28a9NYl6mgnNvNvApO2MAnfkkaZRfzl-GtRn56CFI,5101
79
79
  followthemoney/schema/License.yaml,sha256=bXESXY-JpSmc5sthZe4sssXhx50UoLPAMED9FvEUyRU,534
80
80
  followthemoney/schema/Membership.yaml,sha256=IPmaOX4Ai2r4sGcA5ig2WmLvWHb38akdxp4smEdDWOE,710
81
81
  followthemoney/schema/Mention.yaml,sha256=nBeulR_Jm4x75aJ7yNF0TAVhHJqXQaEzOutLIn_YU-4,1086
82
82
  followthemoney/schema/Message.yaml,sha256=PAxZ2NRFVvnOlp9Ohh5fJDEThjJ0jm3M2YCbJ9KtMuE,1565
83
83
  followthemoney/schema/Note.yaml,sha256=NohwtFupxIssZuEgQowiQWqKit4uQ-OatAu3yp9eJj4,411
84
84
  followthemoney/schema/Occupancy.yaml,sha256=WojlqzuWao84MJxRE9K6a-1D-Jtu78-0h6laODhdKw8,975
85
- followthemoney/schema/Organization.yaml,sha256=wPXU1ni0-3QzvttDq-gIjbAYHzcWoo3nsLGLw6cnHKI,1064
85
+ followthemoney/schema/Organization.yaml,sha256=F_01GsdSsS2cUjJ732R6UfsiodylMITq23EiXdEuoDo,1136
86
86
  followthemoney/schema/Ownership.yaml,sha256=tLWESE9VX0aUuhe6C1pToq2-auPVZBdE3xvBmTRfmPc,1057
87
87
  followthemoney/schema/Package.yaml,sha256=gPr-P3lcg7OOAav_KVa8baK4yK57JwfcXwxXheD96UQ,310
88
88
  followthemoney/schema/Page.yaml,sha256=YjYqaH2sOry0z4xh44CsX_eyuRClD6ZS0d2o2uQXFbo,1062
89
89
  followthemoney/schema/Pages.yaml,sha256=KKPGZ06Ehp5mWIGnYfHUBN9jT03bk8nakw0pB5bA_7E,450
90
90
  followthemoney/schema/Passport.yaml,sha256=rpuLC86sdXnHF-prFQM4mAqYzlSGWKvPE4Cphtn2KRw,805
91
91
  followthemoney/schema/Payment.yaml,sha256=WRBJuj9ljsxLBs-0g9Z9UD87uR1RTtuUiYnWOnKr1qA,1757
92
- followthemoney/schema/Person.yaml,sha256=G6L6bf8WQtOC1Xr1TKWRCJt8JlyQKheBPtH1ZmjjS3w,2132
92
+ followthemoney/schema/Person.yaml,sha256=485xdaX3YYfJNtMkKuwRS78dD2l2bMYjGCmo-NqTvOY,2845
93
93
  followthemoney/schema/PlainText.yaml,sha256=hfnVi-HmQeDbqDquSpkPJax9hNm86ioXGr4hzNzyPFE,278
94
94
  followthemoney/schema/Position.yaml,sha256=ZpxjWOLxwva_on32r9WD5ys0Ty3YxCju41mg9HG-pe0,1308
95
95
  followthemoney/schema/Project.yaml,sha256=2svtyGJopS0UrqPiuYGpBzj30V7k3LRDX4N1U56y4yY,462
96
96
  followthemoney/schema/ProjectParticipant.yaml,sha256=xNehEu90uqUfboNouezhZQ8ZQLxzWq1yyNO4kua-Lyc,727
97
- followthemoney/schema/PublicBody.yaml,sha256=BNfLBqH1OapoEninAjWmqZx_n-G5QUnzzydW7300TiY,301
97
+ followthemoney/schema/PublicBody.yaml,sha256=uw4Ok36E8J5d7tyijHG9Sf_iNFOpUhGpo6UyKgTQp3Q,348
98
98
  followthemoney/schema/RealEstate.yaml,sha256=NWFHXqEHskYQN-kvQESZpu74nztShqoYSZEjZAr-DHM,1363
99
99
  followthemoney/schema/Representation.yaml,sha256=sCvFnUDQaElq2cqSB0rILcMYb2gaMZqlzxlHxyX9IGg,792
100
100
  followthemoney/schema/Risk.yaml,sha256=2BRVBqb6wiLHxb_V50P-YMAOhjC64UVHDyh5PASpCIA,728
@@ -104,7 +104,7 @@ followthemoney/schema/Similar.yaml,sha256=gD8rZEaPQWzU-rEfsKdn62uEucF3KxYBcPMoSd
104
104
  followthemoney/schema/Succession.yaml,sha256=RMJQqZ4Fv88N1RvWTAgjYg9BB5cELSj5CCAjM681Fpg,749
105
105
  followthemoney/schema/Table.yaml,sha256=GcsIAgSO9t2tvObA9zU2HhxlSqTe9CePmUnagu1Z0vI,641
106
106
  followthemoney/schema/TaxRoll.yaml,sha256=ugMzaaS7uyq2OLD50eGLcfvd6Cg0cSt65-T9GVqpRSA,746
107
- followthemoney/schema/Thing.yaml,sha256=hh1oMDQzWiSs1TamBNonmwEdlh2TVrNc3w9hWW8iSeY,2716
107
+ followthemoney/schema/Thing.yaml,sha256=iUaGvGRBqhsVUg-Cx_CNw9WewlWsl9vylU2wiFDQ1qg,2975
108
108
  followthemoney/schema/Trip.yaml,sha256=nLQD_ApmVJ8D56Czl7K700hhNZjzFV9FOQ3NBSQDLiM,771
109
109
  followthemoney/schema/UnknownLink.yaml,sha256=lneS_HZNgeLyJxwzWnLx0ZoyY3MXt99I_K2X_o9z5g8,682
110
110
  followthemoney/schema/UserAccount.yaml,sha256=2bbPKNtt1R3zWSSkaq_SVzRPfFzX74kAxwtIxTymHA8,840
@@ -114,10 +114,10 @@ followthemoney/schema/Vessel.yaml,sha256=zWHUfSK8g6Pz58ZyCaK0AFJ4u_UHjEIUGC4c_7o
114
114
  followthemoney/schema/Video.yaml,sha256=LY3DYMWTHXiAhL0hxBCNCz50cp2sPbUlEhhig5Fbjos,327
115
115
  followthemoney/schema/Workbook.yaml,sha256=iikWPElz4klA7SkWH7eae6xqhbkMCIP_3zdeXzFEMU0,354
116
116
  followthemoney/statement/__init__.py,sha256=7m2VUCAuqNZXIY0WFJRFkw5UG14QuxATL4f_xbqKwhw,633
117
- followthemoney/statement/entity.py,sha256=r01enw01FbSVguvI8WcsSNYoYG-KWPLNNTNwbe7J7I0,17676
117
+ followthemoney/statement/entity.py,sha256=oeudwhqfYLJKqbzxEydasMHqevkDASNyYN6s0yddW6I,18755
118
118
  followthemoney/statement/serialize.py,sha256=9eXzQ1biR2mSxWRID5C7xDdku4b4ZImHeRJ53yLZ0yo,7225
119
119
  followthemoney/statement/statement.py,sha256=Ae-EYuzS8S12BkaRqrvMuI1C7YwlRKa5C_pTBELyNMM,8029
120
- followthemoney/statement/util.py,sha256=B-ozuRc1TWvpop52873Pqt5OPj8H6uk4KyRJLfAhr10,780
120
+ followthemoney/statement/util.py,sha256=QMYSwAcnh2fCM1LtH_-v8Z5GdwOZfUTT1UkQ_ZMQ470,797
121
121
  followthemoney/translations/messages.pot,sha256=JhtY9NJ9wP_EAX4APxOqMyvKcX53oIC9kAxBsliJkf4,107703
122
122
  followthemoney/translations/ar/LC_MESSAGES/followthemoney.mo,sha256=uhb2crSNh8K2ts_QUeD2wvgWgzzpLJWRzXok-Uyx3Zk,38795
123
123
  followthemoney/translations/ar/LC_MESSAGES/followthemoney.po,sha256=DuIfvR5v0sPGwFbeg3y6_jCbeglvHWXQ2LDH6prfwLc,121326
@@ -142,27 +142,27 @@ followthemoney/translations/ru/LC_MESSAGES/followthemoney.po,sha256=7SQWytOTvoAQ
142
142
  followthemoney/translations/tr/LC_MESSAGES/followthemoney.mo,sha256=SC84e_ZF_oFJG1NKdyZY_W6Kb6POORZB6wdeAcEWmnE,487
143
143
  followthemoney/translations/tr/LC_MESSAGES/followthemoney.po,sha256=AZC3marhtVVq8Ck1FOgnt4sbDMz548nX48O9GDwImbQ,89826
144
144
  followthemoney/types/__init__.py,sha256=rWwQeiuMh2BNIuvhpMfJ4bPADDvt9Axu1eedvNFi0qY,3350
145
- followthemoney/types/address.py,sha256=nMFCj5QJyqA1ddpUmDLpRTum0nGXE-J70_WGnaLXnYo,2130
146
- followthemoney/types/checksum.py,sha256=zZrU8WX4CY3Vta_vOyfgDNzIwbmtje7AaDv3O1fBMnk,823
145
+ followthemoney/types/address.py,sha256=Gc-hqz00dRRkeANqkyPD2wtt7ksR9wMf4CX-U-5XvMo,2214
146
+ followthemoney/types/checksum.py,sha256=_0ev2Wwtd4iX_bLz0Lu-xcJIxNfH_V9kBKKtuZhoAwg,802
147
147
  followthemoney/types/common.py,sha256=4ks7zPT8rknrGSd4JFc1zRkS-TL4SX-25_ZbjcVDos0,10081
148
- followthemoney/types/country.py,sha256=n8vihijDVud_3Ra-as4Ize0jf_HbcdKVR5YX3TlKZy0,1533
149
- followthemoney/types/date.py,sha256=PjcaEyW6CBzf0-gHWKUsKjWIaD3AVBEl0zLSRQOVXxc,3105
148
+ followthemoney/types/country.py,sha256=X3Z1j6rIiCITpLtpFXwjTIh9uJwI99_gmPMJx8Jsq2w,1512
149
+ followthemoney/types/date.py,sha256=O3Xav9QNBqjy7LuUWiZrUdGrOvwwOdk6ea5qQEStIwQ,3084
150
150
  followthemoney/types/email.py,sha256=L3RTYrMABlNQF7hCynXGfzoj6YNEHW5JAY_BwuhoZdA,3375
151
- followthemoney/types/entity.py,sha256=oDxVEhuxyU1ScpOpebPpUm3o0I9j_p7Qrq-t5yNpluQ,2338
152
- followthemoney/types/gender.py,sha256=fi9iKLbjAUxDCLBtU1MxWidxv7KgCY2eH5746FYlEGk,1725
153
- followthemoney/types/identifier.py,sha256=hzD188FtwG0w3TcmbnDwnUMc8MZVcWgQJKGAvrwygc4,2296
154
- followthemoney/types/ip.py,sha256=mMFTODFiXAJROCUYJvoLAShyIiTIWVmMBh5zT_GquYM,1300
155
- followthemoney/types/json.py,sha256=V3qJD5RxJykNX51u3w1Nx9xqoNBnkulhzkJI9XMYKFo,1690
156
- followthemoney/types/language.py,sha256=SXgRRH-DyPmyyrqYurSyMiG6WHB8a0Gw81XxroEGD-c,2747
157
- followthemoney/types/mimetype.py,sha256=NdpqVLx3Bre_myYvnbjmdd5wZBf01tllrbhegjO8_m0,1263
158
- followthemoney/types/name.py,sha256=ZWGDebv01qByh_yBYOVoS3Edlm3_JVPShQMklKc6ZOA,2384
159
- followthemoney/types/number.py,sha256=OdVuHDd4IYIIHhx_317JKeMjBAGtsJ2TAcxoZKZ4MkY,3948
160
- followthemoney/types/phone.py,sha256=r8uRqWinS0CYnYBTs405k5gO4jeatUDgjdzzijoMKJE,3811
161
- followthemoney/types/string.py,sha256=fqyTauAm4mNnNaoH-yH087RBbNh-G5ZZUO3awTGQUUg,1230
162
- followthemoney/types/topic.py,sha256=Mi0Gx0m3bDeTmyuvM6jdRMqv81O03U4eI99R13KGu2Y,4503
163
- followthemoney/types/url.py,sha256=QFpS_JIV8unFHuh_uGv22SWUUkocBoOpzLsAJWom_gI,1455
164
- followthemoney-4.2.1.dist-info/METADATA,sha256=M-Hlc_ugS8WclQ3QM9L43YCL4qfegF9FhsGvrTwzWS8,6747
165
- followthemoney-4.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
166
- followthemoney-4.2.1.dist-info/entry_points.txt,sha256=caoFTlf213jhg5sz3TNSofutjUTzaKtWATuSIdd9Cps,653
167
- followthemoney-4.2.1.dist-info/licenses/LICENSE,sha256=H6_EVXisnJC0-18CjXIaqrBSFq_VH3OnS7u3dccOv6g,1148
168
- followthemoney-4.2.1.dist-info/RECORD,,
151
+ followthemoney/types/entity.py,sha256=56h6x8Ct7hWZIC3BjZHmRKGy9Ff2vuULNWH3xDRsKiU,2317
152
+ followthemoney/types/gender.py,sha256=XY9us98Sk25O1xnHN-88tbv9pHy6Mn7SR8GRYi6v5gI,1683
153
+ followthemoney/types/identifier.py,sha256=TYJwE7urjHFxEcDuiZMxGoCN6n34rAIdCt5_96Y7vI0,2198
154
+ followthemoney/types/ip.py,sha256=rCXkRrh_jDeWAhswCgSe6Z4uhIW7yvLAxIEw4x1SM3A,1279
155
+ followthemoney/types/json.py,sha256=Hefwns1-ziJf310MWvdfX5ICkOgj9cnnMJuqq1e6qKY,1676
156
+ followthemoney/types/language.py,sha256=JDFCO9g9lvgKihhYTz6e7TbJd3V9RTGJlS8kDn6aSCY,2726
157
+ followthemoney/types/mimetype.py,sha256=oqVP8EfGckPAI3WAziHomp6oUN7KXdIPWzGZPsRtIA8,1242
158
+ followthemoney/types/name.py,sha256=zd0aC4VGp1SYUI8Rj0-ZXlrpUI7ZcnJIljZqsEsV-CY,2363
159
+ followthemoney/types/number.py,sha256=vpAyhmc7UQlIm8h7Z5k8k4cTk37ykRF-AgYA1r_g1QQ,3934
160
+ followthemoney/types/phone.py,sha256=_HanfxxTV7jp75gZO2evBc9HWwQTxEMQRaoVDcoXDIQ,3790
161
+ followthemoney/types/string.py,sha256=SEh3xqQCnm377PGvwfR6ao85pHJCNeCUWBKnvccrJ7I,1216
162
+ followthemoney/types/topic.py,sha256=9FIH_WmwVOFg1CJRBF4KeE6vNTn-QQkzsKU5XaMqNJ0,4604
163
+ followthemoney/types/url.py,sha256=sSHKtzvm4kc-VTvNCPIDykOG1hUoawhORj6Bklo0a2A,1434
164
+ followthemoney-4.3.4.dist-info/METADATA,sha256=H3K0seI3SN6axQwxgjGK0zErE7ySmoYtOxDom-waNDU,6747
165
+ followthemoney-4.3.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
166
+ followthemoney-4.3.4.dist-info/entry_points.txt,sha256=caoFTlf213jhg5sz3TNSofutjUTzaKtWATuSIdd9Cps,653
167
+ followthemoney-4.3.4.dist-info/licenses/LICENSE,sha256=H6_EVXisnJC0-18CjXIaqrBSFq_VH3OnS7u3dccOv6g,1148
168
+ followthemoney-4.3.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any