followthemoney 3.8.0__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.
@@ -3,7 +3,7 @@ import os
3
3
  from followthemoney.model import Model
4
4
  from followthemoney.util import set_model_locale
5
5
 
6
- __version__ = "3.8.0"
6
+ __version__ = "3.8.1"
7
7
 
8
8
 
9
9
  model_path = os.path.dirname(__file__)
followthemoney/rdf.py CHANGED
@@ -4,6 +4,6 @@ from rdflib import Namespace
4
4
  from rdflib.term import Identifier, URIRef, Literal
5
5
  from rdflib import RDF, SKOS, XSD
6
6
 
7
- NS = Namespace("https://w3id.org/ftm#")
7
+ NS = Namespace("https://schema.followthemoney.tech/#")
8
8
 
9
9
  __all__ = ["NS", "XSD", "RDF", "SKOS", "Identifier", "URIRef", "Literal"]
@@ -21,6 +21,11 @@ Message:
21
21
  - title
22
22
  - threadTopic
23
23
  - fileName
24
+ temporalExtent:
25
+ start:
26
+ - date
27
+ - authoredAt
28
+ - publishedAt
24
29
  properties:
25
30
  subject:
26
31
  label: Subject
@@ -35,6 +35,10 @@ Occupancy:
35
35
  label: "Position holders"
36
36
  type: entity
37
37
  range: Position
38
+ declarationDate:
39
+ label: "Declaration date"
40
+ type: date
41
+ matchable: false
38
42
  status:
39
43
  label: "Status"
40
44
  type: string
@@ -36,6 +36,7 @@ Passport:
36
36
  label: "Place of birth"
37
37
  gender:
38
38
  label: "Gender"
39
+ type: gender
39
40
  personalNumber:
40
41
  label: "Personal number"
41
42
  type: identifier
@@ -7,10 +7,10 @@ Sanction:
7
7
  matchable: false
8
8
  featured:
9
9
  - entity
10
+ - country
10
11
  - authority
11
12
  - program
12
13
  - startDate
13
- - endDate
14
14
  required:
15
15
  - entity
16
16
  caption:
followthemoney/schema.py CHANGED
@@ -105,8 +105,8 @@ class Schema:
105
105
  "edge_source",
106
106
  "edge_target",
107
107
  "edge_caption",
108
- "temporal_start",
109
- "temporal_end",
108
+ "_temporal_start",
109
+ "_temporal_end",
110
110
  "_extends",
111
111
  "extends",
112
112
  "schemata",
@@ -183,8 +183,8 @@ class Schema:
183
183
  #: Specify which properties should be used to represent this schema in a
184
184
  #: timeline.
185
185
  temporal_extent = data.get("temporalExtent", {})
186
- self.temporal_start = set(temporal_extent.get("start", []))
187
- self.temporal_end = set(temporal_extent.get("end", []))
186
+ self._temporal_start = ensure_list(temporal_extent.get("start", []))
187
+ self._temporal_end = ensure_list(temporal_extent.get("end", []))
188
188
 
189
189
  #: Direct parent schemata of this schema.
190
190
  self._extends = ensure_list(data.get("extends", []))
@@ -211,6 +211,8 @@ class Schema:
211
211
  def generate(self, model: "Model") -> None:
212
212
  """While loading the schema, this function will validate and
213
213
  load the hierarchy, properties, and flags of the definition."""
214
+ temporal_start: Optional[List[str]] = None
215
+ temporal_end: Optional[List[str]] = None
214
216
  for extends in self._extends:
215
217
  parent = model.get(extends)
216
218
  if parent is None:
@@ -227,8 +229,22 @@ class Schema:
227
229
  self.names.add(ancestor.name)
228
230
  ancestor.descendants.add(self)
229
231
 
230
- self.temporal_start |= parent.temporal_start
231
- self.temporal_end |= parent.temporal_end
232
+ if len(self._temporal_start) == 0 and parent.temporal_start:
233
+ if (
234
+ temporal_start is not None
235
+ and temporal_start != parent.temporal_start
236
+ ):
237
+ raise InvalidModel(
238
+ "Conflicting temporal start properties: %s" % self.name
239
+ )
240
+ temporal_start = parent.temporal_start
241
+
242
+ if len(self._temporal_end) == 0 and parent.temporal_end:
243
+ if temporal_end is not None and temporal_end != parent.temporal_end:
244
+ raise InvalidModel(
245
+ "Conflicting temporal start properties: %s" % self.name
246
+ )
247
+ temporal_end = parent.temporal_end
232
248
 
233
249
  for prop in list(self.properties.values()):
234
250
  prop.generate(model)
@@ -310,18 +326,38 @@ class Schema:
310
326
  return self.get(self.edge_target)
311
327
 
312
328
  @property
313
- def temporal_start_props(self) -> Set[Property]:
329
+ def temporal_start(self) -> List[str]:
330
+ """The entity properties to be used as the start when representing the entity
331
+ in a timeline."""
332
+ if not len(self._temporal_start):
333
+ for parent in self.extends:
334
+ if len(parent.temporal_start):
335
+ return parent.temporal_start
336
+ return self._temporal_start
337
+
338
+ @property
339
+ def temporal_end(self) -> List[str]:
340
+ """The entity properties to be used as the end when representing the entity
341
+ in a timeline."""
342
+ if not len(self._temporal_end):
343
+ for parent in self.extends:
344
+ if len(parent.temporal_end):
345
+ return parent.temporal_end
346
+ return self._temporal_end
347
+
348
+ @property
349
+ def temporal_start_props(self) -> List[Property]:
314
350
  """The entity properties to be used as the start when representing the entity
315
351
  in a timeline."""
316
352
  props = [self.get(prop_name) for prop_name in self.temporal_start]
317
- return set([prop for prop in props if prop is not None])
353
+ return [prop for prop in props if prop is not None]
318
354
 
319
355
  @property
320
- def temporal_end_props(self) -> Set[Property]:
356
+ def temporal_end_props(self) -> List[Property]:
321
357
  """The entity properties to be used as the end when representing the entity
322
358
  in a timeline."""
323
359
  props = [self.get(prop_name) for prop_name in self.temporal_end]
324
- return set([prop for prop in props if prop is not None])
360
+ return [prop for prop in props if prop is not None]
325
361
 
326
362
  @property
327
363
  def sorted_properties(self) -> List[Property]:
@@ -408,16 +444,10 @@ class Schema:
408
444
  "label": self.edge_label,
409
445
  "directed": self.edge_directed,
410
446
  }
411
- start_props = [
412
- prop.name for prop in self.temporal_start_props if prop.schema == self
413
- ]
414
- end_props = [
415
- prop.name for prop in self.temporal_end_props if prop.schema == self
416
- ]
417
- if start_props or end_props:
447
+ if len(self.temporal_start) or len(self.temporal_end):
418
448
  data["temporalExtent"] = {
419
- "start": sorted(start_props),
420
- "end": sorted(end_props),
449
+ "start": self.temporal_start,
450
+ "end": self.temporal_end,
421
451
  }
422
452
  if len(self.featured):
423
453
  data["featured"] = self.featured
@@ -2,6 +2,8 @@ import re
2
2
  from typing import Optional, TYPE_CHECKING
3
3
  from normality import slugify
4
4
  from normality.cleaning import collapse_spaces
5
+ from rigour.addresses import normalize_address
6
+ from rigour.text.distance import levenshtein_similarity
5
7
 
6
8
  from followthemoney.types.common import PropertyType
7
9
  from followthemoney.util import defer as _
@@ -41,11 +43,18 @@ class AddressType(PropertyType):
41
43
  return None
42
44
  return collapsed
43
45
 
46
+ def compare(self, left: str, right: str) -> float:
47
+ left_norm = normalize_address(left)
48
+ right_norm = normalize_address(right)
49
+ if left_norm is None or right_norm is None:
50
+ return 0.0
51
+ return levenshtein_similarity(left_norm, right_norm, max_edits=3)
52
+
44
53
  def _specificity(self, value: str) -> float:
45
54
  return dampen(10, 60, value)
46
55
 
47
56
  def node_id(self, value: str) -> Optional[str]:
48
- slug = slugify(value)
57
+ slug = slugify(normalize_address(value))
49
58
  if slug is None:
50
59
  return None
51
60
  return f"addr:{value}"
@@ -1,10 +1,11 @@
1
1
  import countrynames
2
2
  from typing import Optional, TYPE_CHECKING
3
3
  from babel.core import Locale
4
+ from rigour.territories import get_territory, get_ftm_countries
4
5
 
5
6
  from followthemoney.rdf import URIRef, Identifier
6
7
  from followthemoney.types.common import EnumType, EnumValues
7
- from followthemoney.util import gettext, defer as _
8
+ from followthemoney.util import defer as _
8
9
 
9
10
  if TYPE_CHECKING:
10
11
  from followthemoney.proxy import EntityProxy
@@ -24,52 +25,7 @@ class CountryType(EnumType):
24
25
  max_length = 16
25
26
 
26
27
  def _locale_names(self, locale: Locale) -> EnumValues:
27
- # extra territories that OCCRP is interested in.
28
- names = {
29
- "zz": gettext("Global"),
30
- "eu": gettext("European Union"),
31
- "un": gettext("United Nations"),
32
- "zr": gettext("Zaire"),
33
- # Overwrite "Czechia" label:
34
- "cz": gettext("Czech Republic"),
35
- "xk": gettext("Kosovo"),
36
- "dd": gettext("East Germany"),
37
- "yucs": gettext("Yugoslavia"),
38
- "csxx": gettext("Serbia and Montenegro"),
39
- "cshh": gettext("Czechoslovakia"),
40
- "suhh": gettext("Soviet Union"),
41
- "ge-ab": gettext("Abkhazia (Occupied Georgia)"),
42
- "x-so": gettext("South Ossetia (Occupied Georgia)"),
43
- "ua-lpr": gettext("Luhansk (Occupied Ukraine)"),
44
- "ua-dpr": gettext("Donetsk (Occupied Ukraine)"),
45
- "ua-cri": gettext("Crimea (Occupied Ukraine)"),
46
- "so-som": gettext("Somaliland"),
47
- "cy-trnc": gettext("Northern Cyprus"),
48
- "az-nk": gettext("Nagorno-Karabakh"),
49
- "iq-kr": gettext("Kurdistan"),
50
- "cn-xz": gettext("Tibet"),
51
- "cq": gettext("Sark"),
52
- "gb-wls": gettext("Wales"),
53
- "gb-sct": gettext("Scotland"),
54
- "gb-nir": gettext("Northern Ireland"),
55
- "md-pmr": gettext("Transnistria (PMR)"),
56
- "pk-km": gettext("Kashmir"),
57
- }
58
- for code, label in locale.territories.items():
59
- code = code.lower()
60
- if code in names:
61
- continue
62
- try:
63
- int(code)
64
- except ValueError:
65
- names[code] = label
66
- # Remove some ISO-3611 codes that are not countries:
67
- names.pop("xa", None)
68
- names.pop("xb", None)
69
- names.pop("qo", None)
70
- names.pop("ea", None)
71
- names.pop("ez", None)
72
- return names
28
+ return {t.code: t.name for t in get_ftm_countries()}
73
29
 
74
30
  def clean_text(
75
31
  self,
@@ -82,6 +38,11 @@ class CountryType(EnumType):
82
38
 
83
39
  The input may be a country code, a country name, etc.
84
40
  """
41
+ territory = get_territory(text)
42
+ if territory is not None:
43
+ ftm_country = territory.ftm_country
44
+ if ftm_country is not None:
45
+ return ftm_country
85
46
  code = countrynames.to_code(text, fuzzy=fuzzy)
86
47
  if code is not None:
87
48
  lower = code.lower()
@@ -93,4 +54,4 @@ class CountryType(EnumType):
93
54
  return value
94
55
 
95
56
  def rdf(self, value: str) -> Identifier:
96
- return URIRef(f"iso-3166-1:{value}")
57
+ return URIRef(f"iso-3166:{value}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: followthemoney
3
- Version: 3.8.0
3
+ Version: 3.8.1
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/alephdata/followthemoney.git
@@ -51,7 +51,7 @@ Requires-Dist: pytz>=2021.1
51
51
  Requires-Dist: pyyaml<7.0.0,>=5.0.0
52
52
  Requires-Dist: rdflib<7.2.0,>=6.2.0
53
53
  Requires-Dist: requests<3.0.0,>=2.21.0
54
- Requires-Dist: rigour<1.0.0,>=0.8.1
54
+ Requires-Dist: rigour<1.0.0,>=0.9.3
55
55
  Requires-Dist: sqlalchemy2-stubs
56
56
  Requires-Dist: sqlalchemy<3.0.0,>=1.4.49
57
57
  Requires-Dist: stringcase<2.0.0,>=1.2.0
@@ -1,4 +1,4 @@
1
- followthemoney/__init__.py,sha256=-5MmNEJ1l81MQncQPSsLxZ-nf7ct3KDn1FrEZ5NRvNc,360
1
+ followthemoney/__init__.py,sha256=Cuo89JNtffF2ZZmMgYEsvlkWKVYHXnFnFvzQ6BRX6Gw,360
2
2
  followthemoney/compare.py,sha256=1GFkCfTzA8QR0CH90kvySR8hvl9hQRUerW5Xw2Ivmpg,5134
3
3
  followthemoney/exc.py,sha256=ynZs_UnTVxHR-iBfat_CpVLraYzVX5yLtVf5Ti14hl4,734
4
4
  followthemoney/graph.py,sha256=VNDKrUBkz_-DmKsr5v-Xm8VfxzabnTwkU_MFk92_TjA,10848
@@ -11,8 +11,8 @@ followthemoney/ontology.py,sha256=7PEoUKISNpkRvVhuLeE3IE9ZiNtdR8mn9_kzZ9yF9l0,29
11
11
  followthemoney/property.py,sha256=zi9ss1v0e8Wmv-FuLtZd2aod5iTLfBekBxuOTgIOUMU,7718
12
12
  followthemoney/proxy.py,sha256=M7RIPF0k-P3v7GYKYhRVVaO1cnUf5FArJepE-2c0KMQ,20033
13
13
  followthemoney/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- followthemoney/rdf.py,sha256=_BzWiBc61UYjiyadvGKya4P4JlJfvZtNrn8SP2hC2JM,328
15
- followthemoney/schema.py,sha256=Tk61vRmWaawW-1HOmu-hbYZtkD8it0fIvNtSRxBEQxA,16774
14
+ followthemoney/rdf.py,sha256=9wPs-tqN9QILuvrmH_YheNaFQctyIQqoIEqcS465QHs,343
15
+ followthemoney/schema.py,sha256=vPG2C4r0ar3F74fbU_v-YPq4I3Qfmu9gzxMFWksU7I4,18097
16
16
  followthemoney/util.py,sha256=dMaCas9EjUPQ9q_a4qtV1pQOxWfxPjyjwR0e84XSN9c,4396
17
17
  followthemoney/cli/__init__.py,sha256=Fl05wMr5-FlOSMRpmu1HJSNfPRpy8u9as5IRbGXdo4U,421
18
18
  followthemoney/cli/aggregate.py,sha256=Gwfi5Bt1LCwqbpsCu4P1Cr-QJtCWhbaqgGEzfwJUUL4,2142
@@ -71,15 +71,15 @@ followthemoney/schema/LegalEntity.yaml,sha256=1AouOu97lbKeLP1VNaprN_oACdTvcptGHo
71
71
  followthemoney/schema/License.yaml,sha256=9Ye5vGEBhi7ttGqf0DdAGCJCN3zz5HtGu52dCcmCsQk,452
72
72
  followthemoney/schema/Membership.yaml,sha256=IPmaOX4Ai2r4sGcA5ig2WmLvWHb38akdxp4smEdDWOE,710
73
73
  followthemoney/schema/Mention.yaml,sha256=nBeulR_Jm4x75aJ7yNF0TAVhHJqXQaEzOutLIn_YU-4,1086
74
- followthemoney/schema/Message.yaml,sha256=89k3kdN2w_Vfz1UjbUUPojZtMsAvmZGiDSkHeSNMl8w,1484
74
+ followthemoney/schema/Message.yaml,sha256=PAxZ2NRFVvnOlp9Ohh5fJDEThjJ0jm3M2YCbJ9KtMuE,1565
75
75
  followthemoney/schema/Note.yaml,sha256=NohwtFupxIssZuEgQowiQWqKit4uQ-OatAu3yp9eJj4,411
76
- followthemoney/schema/Occupancy.yaml,sha256=-alsy15cz3RXC_2xYN6rXxwxSOW_hazCNyIvmp7Jawo,773
76
+ followthemoney/schema/Occupancy.yaml,sha256=hFXUAmPxxy3hNEe92pGKgMp80a9A-v_fNDIZd98NzcY,866
77
77
  followthemoney/schema/Organization.yaml,sha256=wPXU1ni0-3QzvttDq-gIjbAYHzcWoo3nsLGLw6cnHKI,1064
78
78
  followthemoney/schema/Ownership.yaml,sha256=tLWESE9VX0aUuhe6C1pToq2-auPVZBdE3xvBmTRfmPc,1057
79
79
  followthemoney/schema/Package.yaml,sha256=gPr-P3lcg7OOAav_KVa8baK4yK57JwfcXwxXheD96UQ,310
80
80
  followthemoney/schema/Page.yaml,sha256=sQt_CnVyjDVGVECLQoGYZH4hxpjdPhxVRz4XJW-_1OU,1107
81
81
  followthemoney/schema/Pages.yaml,sha256=KKPGZ06Ehp5mWIGnYfHUBN9jT03bk8nakw0pB5bA_7E,450
82
- followthemoney/schema/Passport.yaml,sha256=gFVnLRkNzLfjQqDOArk8IIXghqCTcpAu0TDsizAIju4,786
82
+ followthemoney/schema/Passport.yaml,sha256=rpuLC86sdXnHF-prFQM4mAqYzlSGWKvPE4Cphtn2KRw,805
83
83
  followthemoney/schema/Payment.yaml,sha256=WRBJuj9ljsxLBs-0g9Z9UD87uR1RTtuUiYnWOnKr1qA,1757
84
84
  followthemoney/schema/Person.yaml,sha256=EsMcdOzGa8v8zQ_AgvDHSUFL0SflPMhdSdC7TWwW0sU,2354
85
85
  followthemoney/schema/PlainText.yaml,sha256=hfnVi-HmQeDbqDquSpkPJax9hNm86ioXGr4hzNzyPFE,278
@@ -90,7 +90,7 @@ followthemoney/schema/ProjectParticipant.yaml,sha256=xNehEu90uqUfboNouezhZQ8ZQLx
90
90
  followthemoney/schema/PublicBody.yaml,sha256=BNfLBqH1OapoEninAjWmqZx_n-G5QUnzzydW7300TiY,301
91
91
  followthemoney/schema/RealEstate.yaml,sha256=NWFHXqEHskYQN-kvQESZpu74nztShqoYSZEjZAr-DHM,1363
92
92
  followthemoney/schema/Representation.yaml,sha256=sCvFnUDQaElq2cqSB0rILcMYb2gaMZqlzxlHxyX9IGg,792
93
- followthemoney/schema/Sanction.yaml,sha256=fKkfVPQvY_R2zyQfEkuradilBAwVQWRkWw1Z4_Fn1V8,1259
93
+ followthemoney/schema/Sanction.yaml,sha256=VpvVXReuA3xF1V0tlDyK_LItnL6G_Xt3HivnCf9_zag,1259
94
94
  followthemoney/schema/Security.yaml,sha256=w8Och0cslWjHPAs60HZ6JarEXdIbqGlIbN1NlvgN_7Y,1212
95
95
  followthemoney/schema/Similar.yaml,sha256=gD8rZEaPQWzU-rEfsKdn62uEucF3KxYBcPMoSdnxvME,817
96
96
  followthemoney/schema/Succession.yaml,sha256=RMJQqZ4Fv88N1RvWTAgjYg9BB5cELSj5CCAjM681Fpg,749
@@ -129,10 +129,10 @@ followthemoney/translations/ru/LC_MESSAGES/followthemoney.po,sha256=7SQWytOTvoAQ
129
129
  followthemoney/translations/tr/LC_MESSAGES/followthemoney.mo,sha256=SC84e_ZF_oFJG1NKdyZY_W6Kb6POORZB6wdeAcEWmnE,487
130
130
  followthemoney/translations/tr/LC_MESSAGES/followthemoney.po,sha256=AZC3marhtVVq8Ck1FOgnt4sbDMz548nX48O9GDwImbQ,89826
131
131
  followthemoney/types/__init__.py,sha256=X9XeM6JktzirAw5gGkyDKGM70NuiJ9Tbjoq0IwVclxU,1745
132
- followthemoney/types/address.py,sha256=P6ctzojJks70RZIwk5Bhc9djxejZo4Te0m5pQ9M7bAM,1651
132
+ followthemoney/types/address.py,sha256=NnJli6jIs5DfePL4m3dA3Xwv7GA8sdl1J6Mtm-QLeOY,2068
133
133
  followthemoney/types/checksum.py,sha256=OqjCsBPyMIV3_Y20kTTvjkyayy32pBtaI5KKwYQb6lY,937
134
134
  followthemoney/types/common.py,sha256=Yk-7LZ6uDgFzKXUZxQ_j5miN2ZMnlBOziU0iFC7FE9I,10202
135
- followthemoney/types/country.py,sha256=PlkqZ9_06UO3f47YFYyL3gWwpm151FFlWAV3fodtPfQ,3409
135
+ followthemoney/types/country.py,sha256=1ORYH_wV1cbT7HQzoKi3EbKVAy4G59nsuGk4m6Rm3-M,1834
136
136
  followthemoney/types/date.py,sha256=4lZCd0aws-T3HE2BYkb-a-t8iQDr0nqFSAEBoUKiTlM,2683
137
137
  followthemoney/types/email.py,sha256=zAnMHwC_FZh7IagpDRhGZf-GfQR6uW8d-lZP4UCdGcM,2745
138
138
  followthemoney/types/entity.py,sha256=FA8xXNCkiM7HlF6k5lxTlb_B_LKlUwaoOMaVqAdjgYc,2477
@@ -150,8 +150,8 @@ followthemoney/types/registry.py,sha256=hvQ1oIWz_4PpyUnKA5azbaZCulNb5LCIPyC-AQYV
150
150
  followthemoney/types/string.py,sha256=grDn1OgKWxIzVxGEdp0HjVKIqtQ9W4SW2ty4quv3Pcs,1202
151
151
  followthemoney/types/topic.py,sha256=6g57JKTFF_I233-afvuaswFxMaN_Kmg5MZ4_iAKZh9c,3754
152
152
  followthemoney/types/url.py,sha256=r7Pd6Yfn--amwMi_nHoTLMwm5SH8h50SMgQaa2G4PJ0,1492
153
- followthemoney-3.8.0.dist-info/METADATA,sha256=8VnbpVAf1DUAE8xYD6U2OE-5bBGdGlcMMx10e_EIqaY,6145
154
- followthemoney-3.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
155
- followthemoney-3.8.0.dist-info/entry_points.txt,sha256=xvTXjAz0CiZplq4V3iQXlmBexaJyW3zNucIvcDP6L_c,593
156
- followthemoney-3.8.0.dist-info/licenses/LICENSE,sha256=3tfmmk9RtT1eh67a-NDRwcmOLzztbCtqlHW6O1U92ZA,1098
157
- followthemoney-3.8.0.dist-info/RECORD,,
153
+ followthemoney-3.8.1.dist-info/METADATA,sha256=7gIc__XwnT9cUwDDaG2AQT3zT51gbYTHm1XEd71V6QY,6145
154
+ followthemoney-3.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
155
+ followthemoney-3.8.1.dist-info/entry_points.txt,sha256=xvTXjAz0CiZplq4V3iQXlmBexaJyW3zNucIvcDP6L_c,593
156
+ followthemoney-3.8.1.dist-info/licenses/LICENSE,sha256=3tfmmk9RtT1eh67a-NDRwcmOLzztbCtqlHW6O1U92ZA,1098
157
+ followthemoney-3.8.1.dist-info/RECORD,,