followthemoney 4.0.0__py3-none-any.whl → 4.0.2__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.
@@ -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.0.0"
12
+ __version__ = "4.0.2"
13
13
 
14
14
  # Data model singleton
15
15
  model = Model.instance()
@@ -1,34 +1,34 @@
1
1
  import yaml
2
2
  import logging
3
- from datetime import datetime
4
3
  from functools import cached_property
5
4
  from typing import TYPE_CHECKING
6
5
  from typing_extensions import Self
7
6
  from typing import Any, Dict, List, Optional, Set, Type, TypeVar
8
- from pydantic import BaseModel, HttpUrl, field_validator, model_validator
7
+ from pydantic import BaseModel, field_validator, model_validator
9
8
 
10
9
  from followthemoney.dataset.coverage import DataCoverage
11
10
  from followthemoney.dataset.publisher import DataPublisher
12
11
  from followthemoney.dataset.resource import DataResource
13
- from followthemoney.dataset.util import Named, dataset_name_check
12
+ from followthemoney.dataset.util import Url, DateTimeISO, dataset_name_check
14
13
  from followthemoney.util import PathLike
15
14
 
16
15
  if TYPE_CHECKING:
17
16
  from followthemoney.dataset.catalog import DataCatalog
18
17
 
19
18
  DS = TypeVar("DS", bound="Dataset")
19
+
20
20
  log = logging.getLogger(__name__)
21
21
 
22
22
 
23
23
  class DatasetModel(BaseModel):
24
24
  name: str
25
25
  title: str
26
- license: Optional[HttpUrl] = None
26
+ license: Optional[Url] = None
27
27
  summary: Optional[str] = None
28
28
  description: Optional[str] = None
29
- url: Optional[HttpUrl] = None
30
- updated_at: Optional[datetime] = None
31
- last_export: Optional[datetime] = None
29
+ url: Optional[Url] = None
30
+ updated_at: Optional[DateTimeISO] = None
31
+ last_export: Optional[DateTimeISO] = None
32
32
  entity_count: Optional[int] = None
33
33
  thing_count: Optional[int] = None
34
34
  version: Optional[str] = None
@@ -64,15 +64,13 @@ class DatasetModel(BaseModel):
64
64
  raise ValueError("No resource named %r!" % name)
65
65
 
66
66
 
67
- class Dataset(Named):
67
+ class Dataset:
68
68
  """A container for entities, often from one source or related to one topic.
69
69
  A dataset is a set of data, sez W3C."""
70
70
 
71
- Model = DatasetModel
72
-
73
71
  def __init__(self: Self, data: Dict[str, Any]) -> None:
74
- self.model = self.Model.model_validate(data)
75
- super().__init__(self.model.name)
72
+ self.model = DatasetModel.model_validate(data)
73
+ self.name = self.model.name
76
74
  self.children: Set[Self] = set()
77
75
 
78
76
  @cached_property
@@ -135,3 +133,12 @@ class Dataset(Named):
135
133
 
136
134
  catalog = DataCatalog(cls, {})
137
135
  return catalog.make_dataset(data)
136
+
137
+ def __eq__(self, other: Any) -> bool:
138
+ try:
139
+ return not not self.name == other.name
140
+ except AttributeError:
141
+ return False
142
+
143
+ def __lt__(self, other: Any) -> bool:
144
+ return self.name.__lt__(other.name)
@@ -1,8 +1,8 @@
1
1
  from typing import Optional
2
2
 
3
- from pydantic import BaseModel, HttpUrl
3
+ from pydantic import BaseModel
4
4
 
5
- from followthemoney.dataset.util import CountryCode
5
+ from followthemoney.dataset.util import CountryCode, Url
6
6
  from followthemoney.types import registry
7
7
 
8
8
 
@@ -10,13 +10,13 @@ class DataPublisher(BaseModel):
10
10
  """Publisher information, eg. the government authority."""
11
11
 
12
12
  name: str
13
- url: Optional[HttpUrl] = None
13
+ url: Optional[Url] = None
14
14
  name_en: Optional[str] = None
15
15
  acronym: Optional[str] = None
16
16
  description: Optional[str] = None
17
17
  country: Optional[CountryCode] = None
18
18
  official: Optional[bool] = False
19
- logo_url: Optional[HttpUrl] = None
19
+ logo_url: Optional[Url] = None
20
20
 
21
21
  @property
22
22
  def country_label(self) -> Optional[str]:
@@ -1,7 +1,7 @@
1
- from datetime import datetime
2
1
  from typing import Optional
3
- from pydantic import BaseModel, HttpUrl, field_validator
2
+ from pydantic import BaseModel, field_validator, computed_field
4
3
 
4
+ from followthemoney.dataset.util import Url, DateTimeISO
5
5
  from followthemoney.types import registry
6
6
 
7
7
 
@@ -9,9 +9,9 @@ class DataResource(BaseModel):
9
9
  """A downloadable resource that is part of a dataset."""
10
10
 
11
11
  name: str
12
- url: Optional[HttpUrl] = None
12
+ url: Optional[Url] = None
13
13
  checksum: Optional[str] = None
14
- timestamp: Optional[datetime] = None
14
+ timestamp: Optional[DateTimeISO] = None
15
15
  mime_type: Optional[str] = None
16
16
  title: Optional[str] = None
17
17
  size: Optional[int] = None
@@ -19,10 +19,13 @@ class DataResource(BaseModel):
19
19
  @field_validator("mime_type", mode="after")
20
20
  @classmethod
21
21
  def ensure_mime_type(cls, value: str) -> Optional[str]:
22
- if not registry.mimetype.validate(value):
22
+ cleaned = registry.mimetype.clean_text(value)
23
+ if cleaned is None:
23
24
  raise ValueError(f"Invalid MIME type: {value!r}")
24
- return value
25
+ return cleaned
25
26
 
27
+ # Re: the type: ignore, see https://github.com/python/mypy/issues/1362 and https://docs.pydantic.dev/2.0/usage/computed_fields/
28
+ @computed_field # type: ignore[prop-decorator]
26
29
  @property
27
30
  def mime_type_label(self) -> Optional[str]:
28
31
  if self.mime_type is None:
@@ -1,6 +1,8 @@
1
+ from datetime import datetime
1
2
  from normality import slugify
2
3
  from typing import Annotated, Any
3
- from pydantic import BeforeValidator
4
+ from rigour.time import datetime_iso
5
+ from pydantic import AfterValidator, BeforeValidator, HttpUrl, PlainSerializer
4
6
 
5
7
  from followthemoney.types import registry
6
8
 
@@ -36,23 +38,18 @@ def type_check_country(value: Any) -> str:
36
38
  CountryCode = Annotated[str, BeforeValidator(type_check_country)]
37
39
 
38
40
 
39
- class Named:
40
- name: str
41
+ def type_check_http_url(v: str) -> str:
42
+ url = HttpUrl(v)
43
+ return str(url)
41
44
 
42
- def __init__(self, name: str) -> None:
43
- self.name = name
44
45
 
45
- def __eq__(self, other: Any) -> bool:
46
- try:
47
- return not not self.name == other.name
48
- except AttributeError:
49
- return False
46
+ Url = Annotated[str, AfterValidator(type_check_http_url)]
50
47
 
51
- def __lt__(self, other: Any) -> bool:
52
- return self.name.__lt__(other.name)
53
48
 
54
- def __hash__(self) -> int:
55
- return hash(self.name)
49
+ def serialize_dt(dt: datetime) -> str:
50
+ text = datetime_iso(dt)
51
+ assert text is not None, "Invalid datetime: %r" % dt
52
+ return text
56
53
 
57
- def __repr__(self) -> str:
58
- return f"<{self.__class__.__name__}({self.name!r})>"
54
+
55
+ DateTimeISO = Annotated[datetime, PlainSerializer(serialize_dt)]
followthemoney/entity.py CHANGED
@@ -44,15 +44,20 @@ class ValueEntity(EntityProxy):
44
44
  if stmt_data["prop"] != BASE_ID:
45
45
  self.add(stmt_data["prop"], stmt_data["value"])
46
46
 
47
- def merge(self: "ValueEntity", other: "ValueEntity") -> "ValueEntity":
47
+ def merge(self: VE, other: EntityProxy) -> VE:
48
48
  merged = super().merge(other)
49
- merged._caption = pick_name(_defined(self._caption, other._caption))
50
- merged.referents.update(other.referents)
51
- merged.datasets.update(other.datasets)
52
- self.first_seen = min(_defined(self.first_seen, other.first_seen), default=None)
53
- self.last_seen = max(_defined(self.last_seen, other.last_seen), default=None)
54
- changed = _defined(self.last_change, other.last_change)
55
- self.last_change = max(changed, default=None)
49
+ if isinstance(other, ValueEntity):
50
+ merged._caption = pick_name(_defined(self._caption, other._caption))
51
+ merged.referents.update(other.referents)
52
+ merged.datasets.update(other.datasets)
53
+ merged.first_seen = min(
54
+ _defined(self.first_seen, other.first_seen), default=None
55
+ )
56
+ merged.last_seen = max(
57
+ _defined(self.last_seen, other.last_seen), default=None
58
+ )
59
+ changed = _defined(self.last_change, other.last_change)
60
+ merged.last_change = max(changed, default=None)
56
61
  return merged
57
62
 
58
63
  def to_dict(self) -> Dict[str, Any]:
followthemoney/model.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  import yaml
3
- from functools import lru_cache
3
+ from functools import cache
4
4
  from typing import TYPE_CHECKING, Any
5
5
  from typing import Dict, Generator, Iterator, Optional, Set, TypedDict, Union
6
6
 
@@ -118,7 +118,7 @@ class Model(object):
118
118
  for entity in gen.map(record).values():
119
119
  yield entity
120
120
 
121
- @lru_cache(maxsize=None)
121
+ @cache
122
122
  def common_schema(
123
123
  self, left: Union[str, Schema], right: Union[str, Schema]
124
124
  ) -> Schema:
@@ -0,0 +1,33 @@
1
+ from rigour.names import NamePartTag, NameTypeTag
2
+
3
+ from followthemoney.schema import Schema
4
+
5
+
6
+ # Define the mapping of property names to name part tags.
7
+ # This is used to tag the parts of the name with their type by using
8
+ # `Name.tag_text` with the value of the property to mark name parts.
9
+ PROP_PART_TAGS = (
10
+ ("firstName", NamePartTag.GIVEN),
11
+ ("lastName", NamePartTag.FAMILY),
12
+ ("secondName", NamePartTag.MIDDLE),
13
+ ("middleName", NamePartTag.MIDDLE),
14
+ ("fatherName", NamePartTag.PATRONYMIC),
15
+ ("motherName", NamePartTag.MATRONYMIC),
16
+ ("title", NamePartTag.HONORIFIC),
17
+ ("nameSuffix", NamePartTag.SUFFIX),
18
+ ("weakAlias", NamePartTag.NICK),
19
+ )
20
+
21
+
22
+ def schema_type_tag(schema: Schema) -> NameTypeTag:
23
+ """Return the name type tag for the given schema."""
24
+ if schema.is_a("Person"):
25
+ return NameTypeTag.PER
26
+ elif schema.is_a("Organization"):
27
+ return NameTypeTag.ORG
28
+ elif schema.is_a("LegalEntity"):
29
+ return NameTypeTag.ENT
30
+ elif schema.name in ("Vessel", "Asset", "Airplane", "Security"):
31
+ return NameTypeTag.OBJ
32
+ else:
33
+ return NameTypeTag.UNK
followthemoney/proxy.py CHANGED
@@ -421,7 +421,7 @@ class EntityProxy(object):
421
421
  """Make a deep copy of the current entity proxy."""
422
422
  return self.__class__.from_dict(self.to_dict())
423
423
 
424
- def merge(self: E, other: E) -> E:
424
+ def merge(self: E, other: "EntityProxy") -> E:
425
425
  """Merge another entity proxy into this one. This will try and find
426
426
  the common schema between both entities and then add all property
427
427
  values from the other entity into this one."""
followthemoney/schema.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from typing import TYPE_CHECKING, Any, cast
2
2
  from typing import Dict, List, Optional, Set, TypedDict, Union
3
3
  from banal import ensure_list, ensure_dict, as_bool
4
- from functools import lru_cache
4
+ from functools import cache
5
5
 
6
6
  from followthemoney.property import Property, PropertySpec, PropertyToDict, ReverseSpec
7
7
  from followthemoney.types import registry
@@ -382,12 +382,12 @@ class Schema:
382
382
  self._matchable_schemata.add(schema)
383
383
  return self._matchable_schemata
384
384
 
385
- @lru_cache(maxsize=None)
385
+ @cache
386
386
  def can_match(self, other: "Schema") -> bool:
387
387
  """Check if an schema can match with another schema."""
388
388
  return other in self.matchable_schemata
389
389
 
390
- @lru_cache(maxsize=None)
390
+ @cache
391
391
  def is_a(self, other: Union[str, "Schema"]) -> bool:
392
392
  """Check if the schema or one of its parents is the same as the given
393
393
  candidate ``other``."""
@@ -147,7 +147,7 @@ class StatementEntity(EntityProxy):
147
147
 
148
148
  def add_statement(self, stmt: Statement) -> None:
149
149
  schema = self.schema
150
- if not schema.is_a(stmt.schema):
150
+ if schema.name != stmt.schema and not schema.is_a(stmt.schema):
151
151
  try:
152
152
  self.schema = schema.model.common_schema(schema, stmt.schema)
153
153
  except InvalidData as exc:
@@ -163,7 +163,8 @@ class StatementEntity(EntityProxy):
163
163
  else:
164
164
  self.last_change = max(self.last_change, stmt.first_seen)
165
165
  else:
166
- self._statements.setdefault(stmt.prop, set())
166
+ if stmt.prop not in self._statements:
167
+ self._statements[stmt.prop] = set()
167
168
  self._statements[stmt.prop].add(stmt)
168
169
 
169
170
  def get(self, prop: P, quiet: bool = False) -> List[str]:
@@ -1,6 +1,7 @@
1
1
  import csv
2
2
  import click
3
3
  import orjson
4
+ import logging
4
5
  from io import TextIOWrapper
5
6
  from pathlib import Path
6
7
  from types import TracebackType
@@ -11,6 +12,7 @@ from rigour.boolean import text_bool
11
12
  from followthemoney.statement.statement import Statement, StatementDict
12
13
  from followthemoney.statement.util import unpack_prop
13
14
 
15
+ log = logging.getLogger(__name__)
14
16
 
15
17
  JSON = "json"
16
18
  CSV = "csv"
@@ -85,15 +87,19 @@ def read_pack_statements_decoded(fh: TextIO) -> Generator[Statement, None, None]
85
87
  headers = LEGACY_PACK_COLUMNS
86
88
  continue
87
89
  data = dict(zip(headers, row))
88
- schema, _, prop = unpack_prop(data["prop"])
90
+ try:
91
+ schema, _, prop = unpack_prop(data["prop"])
92
+ except TypeError:
93
+ log.error("Invalid property in pack statement: %s" % data["prop"])
94
+ continue
89
95
  yield Statement(
90
96
  entity_id=data["entity_id"],
91
97
  prop=prop,
92
98
  schema=schema,
93
99
  value=data["value"],
94
100
  dataset=data["dataset"],
95
- lang=data.get("lang") or None,
96
- original_value=data.get("original_value") or None,
101
+ lang=data["lang"] or None,
102
+ original_value=data["original_value"] or None,
97
103
  origin=data.get("origin"),
98
104
  first_seen=data["first_seen"],
99
105
  external=data["external"] == "t",
followthemoney/value.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Any, List, Mapping, Sequence, Set, Union
1
+ from typing import Any, Iterable, List, Mapping, Union
2
2
  from datetime import datetime, date, timezone
3
3
  import typing
4
4
  from prefixdate import DatePrefix
@@ -9,7 +9,7 @@ if typing.TYPE_CHECKING:
9
9
  from followthemoney.proxy import EntityProxy
10
10
 
11
11
  Value = Union[str, int, float, bool, date, datetime, DatePrefix, None, "EntityProxy"]
12
- Values = Union[Value, Sequence[Value], Set[Value]]
12
+ Values = Union[Value, Iterable[Value]]
13
13
 
14
14
 
15
15
  def string_list(value: Any, sanitize: bool = False) -> List[str]:
@@ -59,7 +59,7 @@ def string_list(value: Any, sanitize: bool = False) -> List[str]:
59
59
  if text is None:
60
60
  return []
61
61
  return [text]
62
- if isinstance(value, Sequence):
62
+ if isinstance(value, Iterable):
63
63
  stexts: List[str] = []
64
64
  for inner in value:
65
65
  stexts.extend(string_list(inner, sanitize=sanitize))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: followthemoney
3
- Version: 4.0.0
3
+ Version: 4.0.2
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
@@ -1,19 +1,20 @@
1
- followthemoney/__init__.py,sha256=mFh992fiqvWjdgnIxhapag40tIoj-V7qh9LdMv2JtCM,856
1
+ followthemoney/__init__.py,sha256=-I2qZRQKtvRnPSxLWfOD52OIlJbNTtpNw09EsNvJnmw,856
2
2
  followthemoney/compare.py,sha256=rtITMzJOXLDOSj7yKPfOxFaknIu6kRpiLDIM22zakpI,5619
3
- followthemoney/entity.py,sha256=GThAfNNZ4xdtvKTeue3GbtxCaBPngxhZDItpZnNHTtE,2932
3
+ followthemoney/entity.py,sha256=9wLKE3iFapxRQWOs_OAMzK3wtklf2HXaHaMYydIInWE,3045
4
4
  followthemoney/exc.py,sha256=GyMgwY4QVm87hLevDfV7gM1MJsDqfNCi_UQw7F_A8X8,858
5
5
  followthemoney/graph.py,sha256=7X1CGHGvmktS2LSZqld2iXWzG7B831eCNYyBqamqEJ8,10921
6
6
  followthemoney/helpers.py,sha256=Btb6BlHg_c-qCXZo-NP_LURKG-qu-QD3Fj1ev_c7Xic,7956
7
7
  followthemoney/messages.py,sha256=zUEa9CFecU8nRafIzhN6TKCh1kEihiIyIS1qr8PxY4g,806
8
- followthemoney/model.py,sha256=PD2Oh9B-tGZefB2IqogRDRTplbYZ0j3zgWl16YPEYgs,7396
8
+ followthemoney/model.py,sha256=bWFVNa-DhYzc8BdSXBZdG2ev6Nh9uHx6i4tin8DvEEU,7374
9
+ followthemoney/names.py,sha256=LODQqExKEHdH4z6Mmbhlm0KeKRzGcptaSWzYXZ7lONI,1120
9
10
  followthemoney/namespace.py,sha256=cp7X8aGaZ8HHf7SOfHr2vJHPI2todz2DoyLdiZLNMyg,4472
10
11
  followthemoney/ontology.py,sha256=WWY_PYQGl5Ket4zZBuZglzQxD2Bh9UqHok6GJNNX7GA,3001
11
12
  followthemoney/property.py,sha256=RDTzTXJeeLFLptQL1_gr1S1T-vdDe-8MGMwsRaGQh0I,7665
12
- followthemoney/proxy.py,sha256=Pj1GquUD7gdsM1fXb18QnYeT3E9N4ouhwJmPKNKd8ak,19605
13
+ followthemoney/proxy.py,sha256=KhByvSQk1BrZxTinKjjUsbEuD96RveQQ4LRzQIz6pUA,19617
13
14
  followthemoney/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- followthemoney/schema.py,sha256=8UMkpMK6LeFc-FW0uZkr08NnAN-p30_6thFpuug2Xdw,18127
15
+ followthemoney/schema.py,sha256=WYnPE4Lego0pJHlojECEv0aO9Miw_YIvEb35HoDo4Zk,18087
15
16
  followthemoney/util.py,sha256=DhhcRilSetZpzvCew56AE6zNwenW5a4Y-KtmKM43rjc,4447
16
- followthemoney/value.py,sha256=_JA3DbUVho7xdHqQeYwIaECH2RWqaTiKRJ-LDnWUgaI,2352
17
+ followthemoney/value.py,sha256=BJ4Sj5Tg2kMrslR6FjQUr96d8Kt75U7ny9NgzVGT0ZE,2335
17
18
  followthemoney/cli/__init__.py,sha256=0mmz84uhXRp2qUn3syKnDXofU3MMAAe291s7htqX0Bg,187
18
19
  followthemoney/cli/aggregate.py,sha256=xQTFpU3cVVj7fplpX4OJVrRlTVpn6b9kBr_Vb87pKfg,2164
19
20
  followthemoney/cli/cli.py,sha256=cWSQIrMS0b40uzIveoIfR9CEBbQEwcfonYhDTpioqBM,3584
@@ -25,10 +26,10 @@ followthemoney/cli/util.py,sha256=C3nGMVY3-9JHSFLn3AGvTNcAdvGcgfFS-7jXIzKg6Ik,47
25
26
  followthemoney/dataset/__init__.py,sha256=rOKsI39dccDaYcSa7ASoNKkhmbFYUArxMCRqtrxy2iE,477
26
27
  followthemoney/dataset/catalog.py,sha256=bIpxr0jvJeutNSmCaXREQac7TyvZak2Y_QoCFdCM0d4,3001
27
28
  followthemoney/dataset/coverage.py,sha256=rBnKs7VngCtIuaDqrF5D0ygCHg8NAMkYbmtl7336PSI,724
28
- followthemoney/dataset/dataset.py,sha256=Krqfr_qt9nSGqqU3yr_-eGqZpXeunaXL9JesmYTwi4k,4540
29
- followthemoney/dataset/publisher.py,sha256=AKBLZhhZm3x_onuDyEuZQ2S7lthbp2L5tj8F_TlQDdM,706
30
- followthemoney/dataset/resource.py,sha256=nZgzuBg9IvifJwV8zdJj_njxmlVbUwAjxzgy5J099dE,914
31
- followthemoney/dataset/util.py,sha256=Kc9mUEsimTy3yXIM7hAf35P-sjS40HId87LzhGXnZH4,1650
29
+ followthemoney/dataset/dataset.py,sha256=wWUzWsdzDW9qXLy8lS6Bpy08WMcaNU30oiMXU8jfo14,4724
30
+ followthemoney/dataset/publisher.py,sha256=eHbguTyDRVRC0ohD6phaLIm5d9Y-eJK6XYIQaelrnN4,694
31
+ followthemoney/dataset/resource.py,sha256=S_-tNjMwHQ8LcSOsZO_xhXD-vLK90wyxtIRBbyCJ0Xo,1164
32
+ followthemoney/dataset/util.py,sha256=ajUIBRF64dizdgy9LAp2abvFXRFOWCqQX9sDbToWFYo,1607
32
33
  followthemoney/export/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
34
  followthemoney/export/common.py,sha256=5b-Qlu3MaA0kSzzMAP93FAWncpgiioENnCnHikWYxhs,1021
34
35
  followthemoney/export/csv.py,sha256=reWq1jYIv7sY2PEI4JwIxahYNNqnSiPfMCS3kQX4RZ8,2652
@@ -112,8 +113,8 @@ followthemoney/schema/Vessel.yaml,sha256=nFaUJ_0BzFJstvog1iDvwV9DHKHr9ky4DLb1NZG
112
113
  followthemoney/schema/Video.yaml,sha256=LY3DYMWTHXiAhL0hxBCNCz50cp2sPbUlEhhig5Fbjos,327
113
114
  followthemoney/schema/Workbook.yaml,sha256=iikWPElz4klA7SkWH7eae6xqhbkMCIP_3zdeXzFEMU0,354
114
115
  followthemoney/statement/__init__.py,sha256=PvhLPhmQrezBKCe8rEwJlyTWlrnCzSfyfchVc8gXXEA,568
115
- followthemoney/statement/entity.py,sha256=MHZSWYrnvKZq4o6ZmElEDDyBNfq9MIxxbH8nOGO3OwQ,15465
116
- followthemoney/statement/serialize.py,sha256=sEeyPmjbLNI7_XVT_3p4cdsABESmVvexTdceHcNY6Ok,7041
116
+ followthemoney/statement/entity.py,sha256=92tOai7Yt5GZkOylZcy7866P0iLJsYEzmt-2T7WbXMg,15540
117
+ followthemoney/statement/serialize.py,sha256=9eXzQ1biR2mSxWRID5C7xDdku4b4ZImHeRJ53yLZ0yo,7225
117
118
  followthemoney/statement/statement.py,sha256=Ae-EYuzS8S12BkaRqrvMuI1C7YwlRKa5C_pTBELyNMM,8029
118
119
  followthemoney/statement/util.py,sha256=B-ozuRc1TWvpop52873Pqt5OPj8H6uk4KyRJLfAhr10,780
119
120
  followthemoney/translations/messages.pot,sha256=JhtY9NJ9wP_EAX4APxOqMyvKcX53oIC9kAxBsliJkf4,107703
@@ -159,8 +160,8 @@ followthemoney/types/phone.py,sha256=r8uRqWinS0CYnYBTs405k5gO4jeatUDgjdzzijoMKJE
159
160
  followthemoney/types/string.py,sha256=fqyTauAm4mNnNaoH-yH087RBbNh-G5ZZUO3awTGQUUg,1230
160
161
  followthemoney/types/topic.py,sha256=CS5IoI8gm4MSVxfV6K4mGd20_tT1SaKMkcOt_ObSsAg,3678
161
162
  followthemoney/types/url.py,sha256=QFpS_JIV8unFHuh_uGv22SWUUkocBoOpzLsAJWom_gI,1455
162
- followthemoney-4.0.0.dist-info/METADATA,sha256=SxSYtlQ_s21ID_QHvjb5GMh2oqY5ZTo06vU1K3ZFn7I,6791
163
- followthemoney-4.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
164
- followthemoney-4.0.0.dist-info/entry_points.txt,sha256=caoFTlf213jhg5sz3TNSofutjUTzaKtWATuSIdd9Cps,653
165
- followthemoney-4.0.0.dist-info/licenses/LICENSE,sha256=H6_EVXisnJC0-18CjXIaqrBSFq_VH3OnS7u3dccOv6g,1148
166
- followthemoney-4.0.0.dist-info/RECORD,,
163
+ followthemoney-4.0.2.dist-info/METADATA,sha256=kP9fR9P4N2Tp9BT7vrczM-lBNUS04rFO6Xg-Wt5Wg7c,6791
164
+ followthemoney-4.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
165
+ followthemoney-4.0.2.dist-info/entry_points.txt,sha256=caoFTlf213jhg5sz3TNSofutjUTzaKtWATuSIdd9Cps,653
166
+ followthemoney-4.0.2.dist-info/licenses/LICENSE,sha256=H6_EVXisnJC0-18CjXIaqrBSFq_VH3OnS7u3dccOv6g,1148
167
+ followthemoney-4.0.2.dist-info/RECORD,,