followthemoney 3.5.8__py2.py3-none-any.whl → 3.6.0__py2.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.
- followthemoney/__init__.py +1 -1
- followthemoney/cli/cli.py +18 -14
- followthemoney/export/excel.py +6 -6
- followthemoney/mapping/entity.py +14 -2
- followthemoney/mapping/property.py +15 -3
- followthemoney/mapping/sql.py +1 -1
- followthemoney/property.py +11 -0
- followthemoney/proxy.py +29 -19
- followthemoney/schema/Analyzable.yaml +2 -0
- followthemoney/schema/BankAccount.yaml +3 -0
- followthemoney/schema/LegalEntity.yaml +4 -0
- followthemoney/schema/Person.yaml +4 -0
- followthemoney/schema/Security.yaml +2 -0
- followthemoney/schema/Thing.yaml +1 -0
- followthemoney/translations/ru/LC_MESSAGES/followthemoney.po +24 -22
- followthemoney/types/__init__.py +2 -2
- followthemoney/types/common.py +13 -5
- followthemoney/types/date.py +7 -2
- followthemoney/types/email.py +3 -1
- followthemoney/types/entity.py +3 -1
- followthemoney/types/iban.py +7 -9
- followthemoney/types/identifier.py +17 -0
- followthemoney/types/ip.py +3 -1
- followthemoney/types/language.py +1 -1
- followthemoney/types/mimetype.py +2 -2
- followthemoney/types/name.py +6 -35
- followthemoney/types/phone.py +3 -1
- followthemoney/types/topic.py +6 -1
- followthemoney/types/url.py +5 -21
- {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/METADATA +6 -10
- {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/RECORD +40 -40
- {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/WHEEL +1 -1
- {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/entry_points.txt +0 -1
- tests/types/test_dates.py +5 -5
- tests/types/test_iban.py +2 -1
- tests/types/test_identifiers.py +99 -0
- tests/types/test_names.py +1 -7
- {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/LICENSE +0 -0
- {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/namespace_packages.txt +0 -0
- {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/top_level.txt +0 -0
followthemoney/__init__.py
CHANGED
followthemoney/cli/cli.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
import json
|
|
3
2
|
import click
|
|
3
|
+
import orjson
|
|
4
4
|
import logging
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import Optional,
|
|
6
|
+
from typing import Optional, BinaryIO, List, Any, Dict
|
|
7
7
|
from banal import ensure_list
|
|
8
8
|
|
|
9
9
|
from followthemoney import model
|
|
10
10
|
from followthemoney.namespace import Namespace
|
|
11
|
-
from followthemoney.cli.util import InPath, OutPath, path_entities
|
|
11
|
+
from followthemoney.cli.util import InPath, OutPath, path_entities
|
|
12
12
|
from followthemoney.cli.util import path_writer, write_entity
|
|
13
13
|
from followthemoney.proxy import EntityProxy
|
|
14
14
|
|
|
@@ -20,9 +20,10 @@ def cli() -> None:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@cli.command("dump-model", help="Export the current schema model")
|
|
23
|
-
@click.option("-o", "--outfile", type=click.File("
|
|
24
|
-
def dump_model(outfile:
|
|
25
|
-
|
|
23
|
+
@click.option("-o", "--outfile", type=click.File("wb"), default="-")
|
|
24
|
+
def dump_model(outfile: BinaryIO) -> None:
|
|
25
|
+
f = orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS
|
|
26
|
+
outfile.write(orjson.dumps(model.to_dict(), option=f))
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
@cli.command("validate", help="Re-parse and validate the given data")
|
|
@@ -34,7 +35,7 @@ def validate(infile: Path, outfile: Path) -> None:
|
|
|
34
35
|
for entity in path_entities(infile, EntityProxy, cleaned=False):
|
|
35
36
|
clean = model.make_entity(entity.schema)
|
|
36
37
|
clean.id = entity.id
|
|
37
|
-
for
|
|
38
|
+
for prop, value in entity.itervalues():
|
|
38
39
|
clean.add(prop, value)
|
|
39
40
|
write_entity(outfh, clean)
|
|
40
41
|
except BrokenPipeError:
|
|
@@ -46,12 +47,14 @@ def validate(infile: Path, outfile: Path) -> None:
|
|
|
46
47
|
@click.option("-o", "--outfile", type=OutPath, default="-") # noqa
|
|
47
48
|
def import_vis(infile: Path, outfile: Path) -> None:
|
|
48
49
|
with path_writer(outfile) as outfh:
|
|
49
|
-
with open(infile, "
|
|
50
|
-
data =
|
|
50
|
+
with open(infile, "rb") as infh:
|
|
51
|
+
data: Dict[str, Any] = orjson.loads(infh.read())
|
|
51
52
|
if "entities" in data:
|
|
52
|
-
entities = data.get("entities", data)
|
|
53
|
-
|
|
53
|
+
entities: List[Dict[str, Any]] = data.get("entities", data)
|
|
54
|
+
elif "layout" in data:
|
|
54
55
|
entities = data.get("layout", {}).get("entities", data)
|
|
56
|
+
else:
|
|
57
|
+
raise click.ClickException("No entities found in VIS file")
|
|
55
58
|
for entity_data in ensure_list(entities):
|
|
56
59
|
entity = EntityProxy.from_dict(model, entity_data)
|
|
57
60
|
write_entity(outfh, entity)
|
|
@@ -75,10 +78,11 @@ def sign(infile: Path, outfile: Path, signature: Optional[str]) -> None:
|
|
|
75
78
|
@cli.command(help="Format a stream of entities to make it readable")
|
|
76
79
|
@click.option("-i", "--infile", type=InPath, default="-") # noqa
|
|
77
80
|
def pretty(infile: Path) -> None:
|
|
78
|
-
stdout = click.
|
|
81
|
+
stdout = click.get_binary_stream("stdout")
|
|
79
82
|
try:
|
|
83
|
+
f = orjson.OPT_INDENT_2 | orjson.OPT_APPEND_NEWLINE
|
|
80
84
|
for entity in path_entities(infile, EntityProxy):
|
|
81
|
-
data =
|
|
82
|
-
stdout.write(data
|
|
85
|
+
data = orjson.dumps(entity.to_dict(), option=f)
|
|
86
|
+
stdout.write(data)
|
|
83
87
|
except BrokenPipeError:
|
|
84
88
|
raise click.Abort()
|
followthemoney/export/excel.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from io import BytesIO
|
|
3
3
|
from typing import Dict, List, Optional
|
|
4
|
-
from openpyxl import Workbook
|
|
5
|
-
from openpyxl.cell import WriteOnlyCell
|
|
6
|
-
from openpyxl.styles import Font, PatternFill
|
|
7
|
-
from openpyxl.worksheet.worksheet import Worksheet
|
|
8
|
-
from openpyxl.utils.exceptions import IllegalCharacterError
|
|
4
|
+
from openpyxl import Workbook
|
|
5
|
+
from openpyxl.cell import WriteOnlyCell
|
|
6
|
+
from openpyxl.styles import Font, PatternFill
|
|
7
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
8
|
+
from openpyxl.utils.exceptions import IllegalCharacterError
|
|
9
9
|
|
|
10
10
|
from followthemoney.export.common import Exporter
|
|
11
11
|
from followthemoney.proxy import E
|
|
@@ -25,7 +25,7 @@ class ExcelWriter(object):
|
|
|
25
25
|
self.workbook = Workbook(write_only=True)
|
|
26
26
|
|
|
27
27
|
def make_sheet(self, title: str, headers: List[str]) -> Worksheet:
|
|
28
|
-
sheet = self.workbook.create_sheet(title=title)
|
|
28
|
+
sheet: Worksheet = self.workbook.create_sheet(title=title)
|
|
29
29
|
sheet.freeze_panes = "A2"
|
|
30
30
|
sheet.sheet_properties.filterMode = True
|
|
31
31
|
cells = []
|
followthemoney/mapping/entity.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from hashlib import sha1
|
|
2
3
|
from warnings import warn
|
|
3
4
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set
|
|
@@ -15,6 +16,8 @@ if TYPE_CHECKING:
|
|
|
15
16
|
from followthemoney.model import Model
|
|
16
17
|
from followthemoney.mapping.query import QueryMapping
|
|
17
18
|
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
class EntityMapping(object):
|
|
20
23
|
|
|
@@ -112,16 +115,24 @@ class EntityMapping(object):
|
|
|
112
115
|
# from that accessible to phone and address parsers.
|
|
113
116
|
for prop in self.properties:
|
|
114
117
|
if prop.prop.type == registry.country:
|
|
115
|
-
prop.map(proxy, record, entities)
|
|
118
|
+
discarded_values = prop.map(proxy, record, entities)
|
|
119
|
+
for value in discarded_values:
|
|
120
|
+
log.warn(f"[{self.name}] Discarded unclean value \"{value}\" for property \"{prop.prop.qname}\".")
|
|
116
121
|
|
|
117
122
|
for prop in self.properties:
|
|
118
123
|
if prop.prop.type != registry.country:
|
|
119
|
-
prop.map(proxy, record, entities)
|
|
124
|
+
discarded_values = prop.map(proxy, record, entities)
|
|
125
|
+
for value in discarded_values:
|
|
126
|
+
log.warn(f"[{self.name}] Discarding unclean value \"{value}\" for property \"{prop.prop.qname}\".")
|
|
120
127
|
|
|
121
128
|
# Generate the ID at the end to avoid self-reference checks on empty
|
|
122
129
|
# keys.
|
|
123
130
|
proxy.id = self.compute_key(record)
|
|
124
131
|
if proxy.id is None:
|
|
132
|
+
if self.id_column:
|
|
133
|
+
log.warn(f"[{self.name}] Skipping entity because no ID could be computed. Make sure that there are no empty values in the \"{self.id_column}\" column.")
|
|
134
|
+
if self.keys:
|
|
135
|
+
log.warn(f"[{self.name}] Skipping entity because no ID could be computed. Make sure that there are no empty values in key columns.")
|
|
125
136
|
return None
|
|
126
137
|
|
|
127
138
|
for prop in self.properties:
|
|
@@ -130,6 +141,7 @@ class EntityMapping(object):
|
|
|
130
141
|
# the mapping, not in the model. Basically it means: if
|
|
131
142
|
# this row of source data doesn't have that field, then do
|
|
132
143
|
# not map it again.
|
|
144
|
+
log.warn(f"[{self.name}] Skipping entity because required property \"{prop.prop.name}\" is empty.")
|
|
133
145
|
return None
|
|
134
146
|
return proxy
|
|
135
147
|
|
|
@@ -112,13 +112,13 @@ class PropertyMapping(object):
|
|
|
112
112
|
|
|
113
113
|
def map(
|
|
114
114
|
self, proxy: EntityProxy, record: Record, entities: Dict[str, EntityProxy]
|
|
115
|
-
) ->
|
|
115
|
+
) -> List[str]:
|
|
116
116
|
if self.entity is not None:
|
|
117
117
|
entity = entities.get(self.entity)
|
|
118
118
|
if entity is not None:
|
|
119
119
|
proxy.unsafe_add(self.prop, entity.id, cleaned=True)
|
|
120
120
|
inline_names(proxy, entity)
|
|
121
|
-
return
|
|
121
|
+
return []
|
|
122
122
|
|
|
123
123
|
# clean the values returned by the query, or by using literals, or
|
|
124
124
|
# formats.
|
|
@@ -133,5 +133,17 @@ class PropertyMapping(object):
|
|
|
133
133
|
splote.extend(value.split(self.split))
|
|
134
134
|
values = splote
|
|
135
135
|
|
|
136
|
+
discarded_values: List[str] = []
|
|
137
|
+
|
|
136
138
|
for value in values:
|
|
137
|
-
proxy.unsafe_add(
|
|
139
|
+
added_value = proxy.unsafe_add(
|
|
140
|
+
prop=self.prop,
|
|
141
|
+
value=value,
|
|
142
|
+
fuzzy=self.fuzzy,
|
|
143
|
+
format=self.format,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if value is not None and added_value is None:
|
|
147
|
+
discarded_values.append(value)
|
|
148
|
+
|
|
149
|
+
return discarded_values
|
followthemoney/mapping/sql.py
CHANGED
|
@@ -55,7 +55,7 @@ class SQLSource(Source):
|
|
|
55
55
|
if database is None:
|
|
56
56
|
raise InvalidMapping("No database in SQL mapping!")
|
|
57
57
|
self.database_uri = cast(str, os.path.expandvars(database))
|
|
58
|
-
self.engine = create_engine(self.database_uri, poolclass=NullPool)
|
|
58
|
+
self.engine = create_engine(self.database_uri, poolclass=NullPool)
|
|
59
59
|
self.meta = MetaData()
|
|
60
60
|
|
|
61
61
|
tables = keys_values(data, "table", "tables")
|
followthemoney/property.py
CHANGED
|
@@ -27,6 +27,7 @@ class PropertyDict(TypedDict, total=False):
|
|
|
27
27
|
# stub: Optional[bool]
|
|
28
28
|
rdf: Optional[str]
|
|
29
29
|
range: Optional[str]
|
|
30
|
+
format: Optional[str]
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
class PropertySpec(PropertyDict):
|
|
@@ -58,6 +59,7 @@ class Property:
|
|
|
58
59
|
"matchable",
|
|
59
60
|
"deprecated",
|
|
60
61
|
"_range",
|
|
62
|
+
"format",
|
|
61
63
|
"range",
|
|
62
64
|
"stub",
|
|
63
65
|
"_reverse",
|
|
@@ -113,6 +115,11 @@ class Property:
|
|
|
113
115
|
self._range = data.get("range")
|
|
114
116
|
self.range: Optional["Schema"] = None
|
|
115
117
|
|
|
118
|
+
#: If the property is of type ``identifier``, a more narrow definition of the
|
|
119
|
+
#: identifier format can be provided. For example, LEI, INN or IBAN codes
|
|
120
|
+
#: can be automatically validated.
|
|
121
|
+
self.format: Optional[str] = data.get("format")
|
|
122
|
+
|
|
116
123
|
#: When a property points to another schema, a reverse property is added for
|
|
117
124
|
#: various administrative reasons. These properties are, however, not real
|
|
118
125
|
#: and cannot be written to. That's why they are marked as stubs and adding
|
|
@@ -169,6 +176,8 @@ class Property:
|
|
|
169
176
|
if self.stub:
|
|
170
177
|
return gettext("Property cannot be written")
|
|
171
178
|
val = get_entity_id(val)
|
|
179
|
+
if val is None:
|
|
180
|
+
continue
|
|
172
181
|
if not self.type.validate(val):
|
|
173
182
|
return gettext("Invalid value")
|
|
174
183
|
if val is not None:
|
|
@@ -203,6 +212,8 @@ class Property:
|
|
|
203
212
|
data["range"] = self.range.name
|
|
204
213
|
if self.reverse is not None:
|
|
205
214
|
data["reverse"] = self.reverse.name
|
|
215
|
+
if self.format is not None:
|
|
216
|
+
data["format"] = self.format
|
|
206
217
|
return data
|
|
207
218
|
|
|
208
219
|
def __repr__(self) -> str:
|
followthemoney/proxy.py
CHANGED
|
@@ -194,6 +194,7 @@ class EntityProxy(object):
|
|
|
194
194
|
|
|
195
195
|
for value in value_list(values):
|
|
196
196
|
if not cleaned:
|
|
197
|
+
format = format or prop.format
|
|
197
198
|
value = prop.type.clean(value, proxy=self, fuzzy=fuzzy, format=format)
|
|
198
199
|
self.unsafe_add(prop, value, cleaned=True)
|
|
199
200
|
return None
|
|
@@ -205,26 +206,32 @@ class EntityProxy(object):
|
|
|
205
206
|
cleaned: bool = False,
|
|
206
207
|
fuzzy: bool = False,
|
|
207
208
|
format: Optional[str] = None,
|
|
208
|
-
) ->
|
|
209
|
+
) -> Optional[str]:
|
|
209
210
|
"""A version of `add()` to be used only in type-checking code. This accepts
|
|
210
211
|
only a single value, and performs input cleaning on the premise that the
|
|
211
|
-
value is already valid unicode."""
|
|
212
|
+
value is already valid unicode. Returns the value that has been added."""
|
|
212
213
|
if not cleaned and value is not None:
|
|
214
|
+
format = format or prop.format
|
|
213
215
|
value = prop.type.clean_text(value, fuzzy=fuzzy, format=format, proxy=self)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
216
|
+
|
|
217
|
+
if value is None:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
# Somewhat hacky: limit the maximum size of any particular
|
|
221
|
+
# field to avoid overloading upstream aleph/elasticsearch.
|
|
222
|
+
value_size = len(value)
|
|
223
|
+
if prop.type.max_size is not None:
|
|
224
|
+
if self._size + value_size > prop.type.max_size:
|
|
225
|
+
# msg = "[%s] too large. Rejecting additional values."
|
|
226
|
+
# log.warning(msg, prop.name)
|
|
227
|
+
return None
|
|
228
|
+
self._size += value_size
|
|
229
|
+
self._properties.setdefault(prop.name, list())
|
|
230
|
+
|
|
231
|
+
if value not in self._properties[prop.name]:
|
|
232
|
+
self._properties[prop.name].append(value)
|
|
233
|
+
|
|
234
|
+
return value
|
|
228
235
|
|
|
229
236
|
def set(
|
|
230
237
|
self,
|
|
@@ -424,9 +431,12 @@ class EntityProxy(object):
|
|
|
424
431
|
dictionary can be used to make a new proxy, and it is commonly written to disk
|
|
425
432
|
or a database."""
|
|
426
433
|
data = dict(self.context)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
434
|
+
extra = {
|
|
435
|
+
"id": self.id,
|
|
436
|
+
"schema": self.schema.name,
|
|
437
|
+
"properties": self.properties,
|
|
438
|
+
}
|
|
439
|
+
data.update(extra)
|
|
430
440
|
return data
|
|
431
441
|
|
|
432
442
|
def to_full_dict(self, matchable: bool = False) -> Dict[str, Any]:
|
|
@@ -107,15 +107,18 @@ LegalEntity:
|
|
|
107
107
|
label: "INN"
|
|
108
108
|
description: "Russian company ID"
|
|
109
109
|
type: identifier
|
|
110
|
+
# format: inn
|
|
110
111
|
ogrnCode:
|
|
111
112
|
label: "OGRN"
|
|
112
113
|
description: "Major State Registration Number"
|
|
113
114
|
type: identifier
|
|
115
|
+
# format: ogrn
|
|
114
116
|
leiCode:
|
|
115
117
|
# cf. https://www.gleif.org/en/about-lei/introducing-the-legal-entity-identifier-lei
|
|
116
118
|
label: "LEI"
|
|
117
119
|
description: "Legal Entity Identifier"
|
|
118
120
|
type: identifier
|
|
121
|
+
format: lei
|
|
119
122
|
dunsCode:
|
|
120
123
|
label: "D-U-N-S"
|
|
121
124
|
description: "Dun & Bradstreet identifier"
|
|
@@ -124,6 +127,7 @@ LegalEntity:
|
|
|
124
127
|
label: "SWIFT/BIC"
|
|
125
128
|
description: "Bank identifier code"
|
|
126
129
|
type: identifier
|
|
130
|
+
format: bic
|
|
127
131
|
parent:
|
|
128
132
|
label: "Parent company"
|
|
129
133
|
description: "If this entity is a subsidiary, another entity (company or organisation) is its parent"
|
|
@@ -26,6 +26,10 @@ Person:
|
|
|
26
26
|
title:
|
|
27
27
|
label: Title
|
|
28
28
|
rdf: http://xmlns.com/foaf/0.1/title
|
|
29
|
+
# The `firstName`, `lastName`, `secondName` etc. properties intentionally do not use
|
|
30
|
+
# the `name` property type. Many FtM tools (including Aleph) use name properties to
|
|
31
|
+
# compare/match entities, but matching entites just on e.g. a first name would lead to
|
|
32
|
+
# too many false positives.
|
|
29
33
|
firstName:
|
|
30
34
|
label: First name
|
|
31
35
|
rdf: http://xmlns.com/foaf/0.1/givenName
|
|
@@ -23,6 +23,7 @@ Security:
|
|
|
23
23
|
label: ISIN
|
|
24
24
|
description: International Securities Identification Number
|
|
25
25
|
type: identifier
|
|
26
|
+
format: isin
|
|
26
27
|
registrationNumber:
|
|
27
28
|
label: Registration number
|
|
28
29
|
type: identifier
|
|
@@ -32,6 +33,7 @@ Security:
|
|
|
32
33
|
figiCode:
|
|
33
34
|
label: Financial Instrument Global Identifier
|
|
34
35
|
type: identifier
|
|
36
|
+
format: figi
|
|
35
37
|
issuer:
|
|
36
38
|
label: "Issuer"
|
|
37
39
|
type: entity
|
followthemoney/schema/Thing.yaml
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
|
|
5
5
|
#
|
|
6
6
|
# Translators:
|
|
7
|
-
# pudo <friedrich@pudo.org>,
|
|
8
|
-
# jen occrp,
|
|
7
|
+
# pudo <friedrich@pudo.org>, 2022
|
|
8
|
+
# jen occrp, 2024
|
|
9
9
|
#
|
|
10
10
|
#, fuzzy
|
|
11
11
|
msgid ""
|
|
@@ -13,8 +13,8 @@ msgstr ""
|
|
|
13
13
|
"Project-Id-Version: PROJECT VERSION\n"
|
|
14
14
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
|
15
15
|
"POT-Creation-Date: 2024-01-04 14:09+0100\n"
|
|
16
|
-
"PO-Revision-Date:
|
|
17
|
-
"Last-Translator: jen occrp,
|
|
16
|
+
"PO-Revision-Date: 2022-11-21 11:38+0000\n"
|
|
17
|
+
"Last-Translator: jen occrp, 2024\n"
|
|
18
18
|
"Language-Team: Russian (https://app.transifex.com/aleph/teams/76591/ru/)\n"
|
|
19
19
|
"MIME-Version: 1.0\n"
|
|
20
20
|
"Content-Type: text/plain; charset=UTF-8\n"
|
|
@@ -1029,17 +1029,17 @@ msgstr "Тикер"
|
|
|
1029
1029
|
#. Company.properties.permId.label
|
|
1030
1030
|
#: followthemoney/schema/Company.yaml
|
|
1031
1031
|
msgid "PermID"
|
|
1032
|
-
msgstr ""
|
|
1032
|
+
msgstr "PermID"
|
|
1033
1033
|
|
|
1034
1034
|
#. Company.properties.permId.description
|
|
1035
1035
|
#: followthemoney/schema/Company.yaml
|
|
1036
1036
|
msgid "LSEG/Refinitiv code for a company"
|
|
1037
|
-
msgstr ""
|
|
1037
|
+
msgstr "LSEG/Refinitiv код компании"
|
|
1038
1038
|
|
|
1039
1039
|
#. Company.properties.ricCode.label
|
|
1040
1040
|
#: followthemoney/schema/Company.yaml
|
|
1041
1041
|
msgid "Reuters Instrument Code"
|
|
1042
|
-
msgstr ""
|
|
1042
|
+
msgstr "RIC"
|
|
1043
1043
|
|
|
1044
1044
|
#. Contract.label
|
|
1045
1045
|
#. ContractAward.properties.contract.label
|
|
@@ -2779,7 +2779,7 @@ msgstr "Должностное лицо"
|
|
|
2779
2779
|
#. Occupancy.properties.holder.reverse.label
|
|
2780
2780
|
#: followthemoney/schema/Occupancy.yaml
|
|
2781
2781
|
msgid "Positions held"
|
|
2782
|
-
msgstr ""
|
|
2782
|
+
msgstr "Занимаемые должности"
|
|
2783
2783
|
|
|
2784
2784
|
#. Occupancy.properties.post.label
|
|
2785
2785
|
#: followthemoney/schema/Occupancy.yaml
|
|
@@ -2789,7 +2789,7 @@ msgstr "Занимаемая должность"
|
|
|
2789
2789
|
#. Occupancy.properties.post.reverse.label
|
|
2790
2790
|
#: followthemoney/schema/Occupancy.yaml
|
|
2791
2791
|
msgid "Position holders"
|
|
2792
|
-
msgstr ""
|
|
2792
|
+
msgstr "Занимающие должность"
|
|
2793
2793
|
|
|
2794
2794
|
#. Organization.plural
|
|
2795
2795
|
#: followthemoney/schema/Organization.yaml
|
|
@@ -3366,18 +3366,20 @@ msgstr "Дата записи"
|
|
|
3366
3366
|
#. RealEstate.properties.parent.label
|
|
3367
3367
|
#: followthemoney/schema/RealEstate.yaml
|
|
3368
3368
|
msgid "Parent unit"
|
|
3369
|
-
msgstr ""
|
|
3369
|
+
msgstr "Основной объект недвижимости"
|
|
3370
3370
|
|
|
3371
3371
|
#. RealEstate.properties.parent.description
|
|
3372
3372
|
#: followthemoney/schema/RealEstate.yaml
|
|
3373
3373
|
msgid ""
|
|
3374
3374
|
"If this entity is a subunit, another entity (real estate) is its parent"
|
|
3375
3375
|
msgstr ""
|
|
3376
|
+
"Вспомогательный объект недвижимости по отношению к другому — основному "
|
|
3377
|
+
"объекту недвижимости"
|
|
3376
3378
|
|
|
3377
3379
|
#. RealEstate.properties.parent.reverse.label
|
|
3378
3380
|
#: followthemoney/schema/RealEstate.yaml
|
|
3379
3381
|
msgid "Subunits"
|
|
3380
|
-
msgstr ""
|
|
3382
|
+
msgstr "Вспомогательные объекты недвижимости"
|
|
3381
3383
|
|
|
3382
3384
|
#. Representation.label
|
|
3383
3385
|
#: followthemoney/schema/Representation.yaml
|
|
@@ -3486,7 +3488,7 @@ msgstr "Котируемый финансовый актив."
|
|
|
3486
3488
|
#. Security.properties.figiCode.label
|
|
3487
3489
|
#: followthemoney/schema/Security.yaml
|
|
3488
3490
|
msgid "Financial Instrument Global Identifier"
|
|
3489
|
-
msgstr ""
|
|
3491
|
+
msgstr "Глобальный идентификатор финансового инструмента"
|
|
3490
3492
|
|
|
3491
3493
|
#. Security.properties.issuer.label
|
|
3492
3494
|
#: followthemoney/schema/Security.yaml
|
|
@@ -4240,7 +4242,7 @@ msgstr "Финансовые преступления"
|
|
|
4240
4242
|
|
|
4241
4243
|
#: followthemoney/types/topic.py:30
|
|
4242
4244
|
msgid "Environmental violations"
|
|
4243
|
-
msgstr ""
|
|
4245
|
+
msgstr "Нарушения в области охраны окружающей среды"
|
|
4244
4246
|
|
|
4245
4247
|
#: followthemoney/types/topic.py:31
|
|
4246
4248
|
msgid "Theft"
|
|
@@ -4280,7 +4282,7 @@ msgstr "Офшорная компания"
|
|
|
4280
4282
|
|
|
4281
4283
|
#: followthemoney/types/topic.py:40
|
|
4282
4284
|
msgid "Public listed company"
|
|
4283
|
-
msgstr ""
|
|
4285
|
+
msgstr "Компания, зарегистрированная на фондовой бирже"
|
|
4284
4286
|
|
|
4285
4287
|
#: followthemoney/types/topic.py:41
|
|
4286
4288
|
msgid "Government"
|
|
@@ -4308,31 +4310,31 @@ msgstr "Межправительственная организация"
|
|
|
4308
4310
|
|
|
4309
4311
|
#: followthemoney/types/topic.py:47
|
|
4310
4312
|
msgid "Head of government or state"
|
|
4311
|
-
msgstr ""
|
|
4313
|
+
msgstr "Глава правительства или государства"
|
|
4312
4314
|
|
|
4313
4315
|
#: followthemoney/types/topic.py:48
|
|
4314
4316
|
msgid "Civil service"
|
|
4315
|
-
msgstr ""
|
|
4317
|
+
msgstr "Государственная служба"
|
|
4316
4318
|
|
|
4317
4319
|
#: followthemoney/types/topic.py:49
|
|
4318
4320
|
msgid "Executive branch of government"
|
|
4319
|
-
msgstr ""
|
|
4321
|
+
msgstr "Исполнительная власть"
|
|
4320
4322
|
|
|
4321
4323
|
#: followthemoney/types/topic.py:50
|
|
4322
4324
|
msgid "Legislative branch of government"
|
|
4323
|
-
msgstr ""
|
|
4325
|
+
msgstr "Законодательная власть"
|
|
4324
4326
|
|
|
4325
4327
|
#: followthemoney/types/topic.py:51
|
|
4326
4328
|
msgid "Judicial branch of government"
|
|
4327
|
-
msgstr ""
|
|
4329
|
+
msgstr "Судебная власть"
|
|
4328
4330
|
|
|
4329
4331
|
#: followthemoney/types/topic.py:52
|
|
4330
4332
|
msgid "Security services"
|
|
4331
|
-
msgstr ""
|
|
4333
|
+
msgstr "Спецслужбы"
|
|
4332
4334
|
|
|
4333
4335
|
#: followthemoney/types/topic.py:53
|
|
4334
4336
|
msgid "Central banking and financial integrity"
|
|
4335
|
-
msgstr ""
|
|
4337
|
+
msgstr "Центральный банк и финансовая безупречность"
|
|
4336
4338
|
|
|
4337
4339
|
#: followthemoney/types/topic.py:54
|
|
4338
4340
|
msgid "Financial services"
|
|
@@ -4416,7 +4418,7 @@ msgstr "Подсанкционное физическое или юридиче
|
|
|
4416
4418
|
|
|
4417
4419
|
#: followthemoney/types/topic.py:76
|
|
4418
4420
|
msgid "Export controlled"
|
|
4419
|
-
msgstr ""
|
|
4421
|
+
msgstr "Экспортный контроль"
|
|
4420
4422
|
|
|
4421
4423
|
#: followthemoney/types/topic.py:77
|
|
4422
4424
|
msgid "Debarred entity"
|
followthemoney/types/__init__.py
CHANGED
|
@@ -3,7 +3,6 @@ from followthemoney.types.url import UrlType
|
|
|
3
3
|
from followthemoney.types.name import NameType
|
|
4
4
|
from followthemoney.types.email import EmailType
|
|
5
5
|
from followthemoney.types.ip import IpType
|
|
6
|
-
from followthemoney.types.iban import IbanType
|
|
7
6
|
from followthemoney.types.address import AddressType
|
|
8
7
|
from followthemoney.types.date import DateType
|
|
9
8
|
from followthemoney.types.phone import PhoneType
|
|
@@ -12,6 +11,7 @@ from followthemoney.types.language import LanguageType
|
|
|
12
11
|
from followthemoney.types.mimetype import MimeType
|
|
13
12
|
from followthemoney.types.checksum import ChecksumType
|
|
14
13
|
from followthemoney.types.identifier import IdentifierType
|
|
14
|
+
from followthemoney.types.iban import IbanType
|
|
15
15
|
from followthemoney.types.entity import EntityType
|
|
16
16
|
from followthemoney.types.topic import TopicType
|
|
17
17
|
from followthemoney.types.gender import GenderType
|
|
@@ -27,7 +27,6 @@ registry.add(UrlType)
|
|
|
27
27
|
registry.add(NameType)
|
|
28
28
|
registry.add(EmailType)
|
|
29
29
|
registry.add(IpType)
|
|
30
|
-
registry.add(IbanType)
|
|
31
30
|
registry.add(AddressType)
|
|
32
31
|
registry.add(DateType)
|
|
33
32
|
registry.add(PhoneType)
|
|
@@ -36,6 +35,7 @@ registry.add(LanguageType)
|
|
|
36
35
|
registry.add(MimeType)
|
|
37
36
|
registry.add(ChecksumType)
|
|
38
37
|
registry.add(IdentifierType)
|
|
38
|
+
registry.add(IbanType) # TODO: remove
|
|
39
39
|
registry.add(EntityType)
|
|
40
40
|
registry.add(TopicType)
|
|
41
41
|
registry.add(GenderType)
|
followthemoney/types/common.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from inspect import cleandoc
|
|
1
2
|
from itertools import product
|
|
2
3
|
from babel.core import Locale
|
|
3
4
|
from banal import ensure_list
|
|
@@ -64,12 +65,17 @@ class PropertyType(object):
|
|
|
64
65
|
|
|
65
66
|
@property
|
|
66
67
|
def docs(self) -> Optional[str]:
|
|
67
|
-
|
|
68
|
+
if not self.__doc__:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
return cleandoc(self.__doc__)
|
|
68
72
|
|
|
69
|
-
def validate(
|
|
73
|
+
def validate(
|
|
74
|
+
self, value: str, fuzzy: bool = False, format: Optional[str] = None
|
|
75
|
+
) -> bool:
|
|
70
76
|
"""Returns a boolean to indicate if the given value is a valid instance of
|
|
71
77
|
the type."""
|
|
72
|
-
cleaned = self.clean(value)
|
|
78
|
+
cleaned = self.clean(value, fuzzy=fuzzy, format=format)
|
|
73
79
|
return cleaned is not None
|
|
74
80
|
|
|
75
81
|
def clean(
|
|
@@ -141,7 +147,7 @@ class PropertyType(object):
|
|
|
141
147
|
) -> float:
|
|
142
148
|
"""Compare two sets of values and select the highest-scored result."""
|
|
143
149
|
results = []
|
|
144
|
-
for
|
|
150
|
+
for l, r in product(ensure_list(left), ensure_list(right)):
|
|
145
151
|
results.append(self.compare(l, r))
|
|
146
152
|
if not len(results):
|
|
147
153
|
return 0.0
|
|
@@ -229,7 +235,9 @@ class EnumType(PropertyType):
|
|
|
229
235
|
self._names[locale] = self._locale_names(locale)
|
|
230
236
|
return self._names[locale]
|
|
231
237
|
|
|
232
|
-
def validate(
|
|
238
|
+
def validate(
|
|
239
|
+
self, value: str, fuzzy: bool = False, format: Optional[str] = None
|
|
240
|
+
) -> bool:
|
|
233
241
|
"""Make sure that the given code value is one of the supported set."""
|
|
234
242
|
if value is None:
|
|
235
243
|
return False
|
followthemoney/types/date.py
CHANGED
|
@@ -27,9 +27,14 @@ class DateType(PropertyType):
|
|
|
27
27
|
plural = _("Dates")
|
|
28
28
|
matchable = True
|
|
29
29
|
|
|
30
|
-
def validate(
|
|
30
|
+
def validate(
|
|
31
|
+
self, value: str, fuzzy: bool = False, format: Optional[str] = None
|
|
32
|
+
) -> bool:
|
|
31
33
|
"""Check if a thing is a valid date."""
|
|
32
|
-
|
|
34
|
+
if format is not None:
|
|
35
|
+
prefix = parse_format(value, format)
|
|
36
|
+
else:
|
|
37
|
+
prefix = parse(value)
|
|
33
38
|
return prefix.precision != Precision.EMPTY
|
|
34
39
|
|
|
35
40
|
def clean_text(
|