followthemoney 3.5.9__py2.py3-none-any.whl → 3.6.1__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.
Potentially problematic release.
This version of followthemoney might be problematic. Click here for more details.
- 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/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 +11 -35
- followthemoney/types/phone.py +3 -1
- followthemoney/types/topic.py +5 -1
- followthemoney/types/url.py +5 -21
- {followthemoney-3.5.9.dist-info → followthemoney-3.6.1.dist-info}/METADATA +6 -6
- {followthemoney-3.5.9.dist-info → followthemoney-3.6.1.dist-info}/RECORD +38 -38
- {followthemoney-3.5.9.dist-info → followthemoney-3.6.1.dist-info}/WHEEL +1 -1
- tests/types/test_iban.py +2 -1
- tests/types/test_identifiers.py +99 -0
- tests/types/test_names.py +1 -7
- {followthemoney-3.5.9.dist-info → followthemoney-3.6.1.dist-info}/LICENSE +0 -0
- {followthemoney-3.5.9.dist-info → followthemoney-3.6.1.dist-info}/entry_points.txt +0 -0
- {followthemoney-3.5.9.dist-info → followthemoney-3.6.1.dist-info}/namespace_packages.txt +0 -0
- {followthemoney-3.5.9.dist-info → followthemoney-3.6.1.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
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(
|
followthemoney/types/email.py
CHANGED
|
@@ -36,7 +36,9 @@ class EmailType(PropertyType):
|
|
|
36
36
|
# except:
|
|
37
37
|
# return False
|
|
38
38
|
|
|
39
|
-
def validate(
|
|
39
|
+
def validate(
|
|
40
|
+
self, value: str, fuzzy: bool = False, format: Optional[str] = None
|
|
41
|
+
) -> bool:
|
|
40
42
|
"""Check to see if this is a valid email address."""
|
|
41
43
|
# TODO: adopt email.utils.parseaddr
|
|
42
44
|
email = sanitize_text(value)
|
followthemoney/types/entity.py
CHANGED
|
@@ -29,7 +29,9 @@ class EntityType(PropertyType):
|
|
|
29
29
|
matchable = True
|
|
30
30
|
pivot = True
|
|
31
31
|
|
|
32
|
-
def validate(
|
|
32
|
+
def validate(
|
|
33
|
+
self, value: str, fuzzy: bool = False, format: Optional[str] = None
|
|
34
|
+
) -> bool:
|
|
33
35
|
text = sanitize_text(value)
|
|
34
36
|
if text is None:
|
|
35
37
|
return False
|
followthemoney/types/iban.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
from typing import Optional, TYPE_CHECKING
|
|
2
|
-
from
|
|
3
|
-
from stdnum.exceptions import ValidationError # type: ignore
|
|
1
|
+
from typing import Optional, TYPE_CHECKING
|
|
2
|
+
from rigour.ids import IBAN
|
|
4
3
|
|
|
5
4
|
from followthemoney.types.common import PropertyType
|
|
6
5
|
from followthemoney.rdf import URIRef, Identifier
|
|
@@ -26,12 +25,11 @@ class IbanType(PropertyType):
|
|
|
26
25
|
matchable = True
|
|
27
26
|
pivot = True
|
|
28
27
|
|
|
29
|
-
def validate(self, value: str) -> bool:
|
|
28
|
+
def validate(self, value: str, fuzzy: bool = False, format: Optional[str] = None) -> bool:
|
|
30
29
|
text = sanitize_text(value)
|
|
31
|
-
|
|
32
|
-
return cast(bool, iban.validate(text))
|
|
33
|
-
except ValidationError:
|
|
30
|
+
if text is None:
|
|
34
31
|
return False
|
|
32
|
+
return IBAN.is_valid(text)
|
|
35
33
|
|
|
36
34
|
def clean_text(
|
|
37
35
|
self,
|
|
@@ -42,7 +40,7 @@ class IbanType(PropertyType):
|
|
|
42
40
|
) -> Optional[str]:
|
|
43
41
|
"""Create a more clean, but still user-facing version of an
|
|
44
42
|
instance of the type."""
|
|
45
|
-
return
|
|
43
|
+
return IBAN.normalize(text)
|
|
46
44
|
|
|
47
45
|
def country_hint(self, value: str) -> str:
|
|
48
46
|
return value[:2].lower()
|
|
@@ -54,4 +52,4 @@ class IbanType(PropertyType):
|
|
|
54
52
|
return f"iban:{value.upper()}"
|
|
55
53
|
|
|
56
54
|
def caption(self, value: str) -> str:
|
|
57
|
-
return
|
|
55
|
+
return IBAN.format(value)
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import re
|
|
2
|
+
from typing import Optional, TYPE_CHECKING
|
|
3
|
+
from rigour.ids import get_identifier_format_names, get_identifier_format
|
|
2
4
|
|
|
3
5
|
from followthemoney.types.common import PropertyType
|
|
4
6
|
from followthemoney.util import dampen, shortest, longest
|
|
5
7
|
from followthemoney.util import defer as _
|
|
6
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from followthemoney.proxy import EntityProxy
|
|
11
|
+
|
|
7
12
|
|
|
8
13
|
class IdentifierType(PropertyType):
|
|
9
14
|
"""Used for registration numbers and other codes assigned by an authority
|
|
@@ -22,6 +27,18 @@ class IdentifierType(PropertyType):
|
|
|
22
27
|
matchable = True
|
|
23
28
|
pivot = True
|
|
24
29
|
|
|
30
|
+
def clean_text(
|
|
31
|
+
self,
|
|
32
|
+
text: str,
|
|
33
|
+
fuzzy: bool = False,
|
|
34
|
+
format: Optional[str] = None,
|
|
35
|
+
proxy: Optional["EntityProxy"] = None,
|
|
36
|
+
) -> Optional[str]:
|
|
37
|
+
if format in get_identifier_format_names():
|
|
38
|
+
format_ = get_identifier_format(format)
|
|
39
|
+
return format_.normalize(text)
|
|
40
|
+
return text
|
|
41
|
+
|
|
25
42
|
def clean_compare(self, value: str) -> str:
|
|
26
43
|
# TODO: should this be used for normalization?
|
|
27
44
|
value = self.COMPARE_CLEAN.sub("", value)
|
followthemoney/types/ip.py
CHANGED
|
@@ -21,7 +21,9 @@ class IpType(PropertyType):
|
|
|
21
21
|
matchable = True
|
|
22
22
|
pivot = True
|
|
23
23
|
|
|
24
|
-
def validate(
|
|
24
|
+
def validate(
|
|
25
|
+
self, value: str, fuzzy: bool = False, format: Optional[str] = None
|
|
26
|
+
) -> bool:
|
|
25
27
|
"""Check to see if this is a valid ip address."""
|
|
26
28
|
try:
|
|
27
29
|
ip_address(value)
|
followthemoney/types/language.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Optional, TYPE_CHECKING
|
|
2
2
|
from babel.core import Locale
|
|
3
|
-
from
|
|
3
|
+
from rigour.langs import iso_639_alpha3
|
|
4
4
|
|
|
5
5
|
from followthemoney.types.common import EnumType, EnumValues
|
|
6
6
|
from followthemoney.rdf import URIRef, Identifier
|
followthemoney/types/mimetype.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Optional, TYPE_CHECKING
|
|
2
|
-
from
|
|
3
|
-
from
|
|
2
|
+
from rigour.mime import normalize_mimetype, parse_mimetype
|
|
3
|
+
from rigour.mime import DEFAULT
|
|
4
4
|
|
|
5
5
|
from followthemoney.types.common import PropertyType
|
|
6
6
|
from followthemoney.rdf import URIRef, Identifier
|
followthemoney/types/name.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING,
|
|
2
|
-
|
|
3
|
-
from
|
|
1
|
+
from typing import TYPE_CHECKING, Optional, Sequence
|
|
2
|
+
from rigour.text.distance import levenshtein_similarity
|
|
3
|
+
from rigour.names import pick_name
|
|
4
4
|
from normality import slugify
|
|
5
5
|
from normality.cleaning import collapse_spaces, strip_quotes
|
|
6
|
+
from fingerprints.cleanup import clean_name_light
|
|
6
7
|
|
|
7
8
|
from followthemoney.types.common import PropertyType
|
|
8
9
|
from followthemoney.util import dampen
|
|
@@ -40,44 +41,19 @@ class NameType(PropertyType):
|
|
|
40
41
|
|
|
41
42
|
def pick(self, values: Sequence[str]) -> Optional[str]:
|
|
42
43
|
"""From a set of names, pick the most plausible user-facing one."""
|
|
43
|
-
|
|
44
|
-
values = sorted(values)
|
|
45
|
-
if not len(values):
|
|
46
|
-
return None
|
|
47
|
-
normalised: List[Union[str, bytes]] = []
|
|
48
|
-
lookup: Dict[str, List[Union[str, bytes]]] = {}
|
|
49
|
-
# We're doing this in two stages, to avoid name forms with varied casing
|
|
50
|
-
# (e.g. Smith vs. SMITH) are counted as widly different, leading to
|
|
51
|
-
# implausible median outcomes.
|
|
52
|
-
for value in values:
|
|
53
|
-
norm = slugify(value, sep=" ")
|
|
54
|
-
if norm is None:
|
|
55
|
-
continue
|
|
56
|
-
normalised.append(norm)
|
|
57
|
-
lookup.setdefault(norm, [])
|
|
58
|
-
lookup[norm].append(value)
|
|
59
|
-
|
|
60
|
-
if not normalised:
|
|
61
|
-
return None
|
|
62
|
-
|
|
63
|
-
norm = setmedian(normalised)
|
|
64
|
-
if norm is None:
|
|
65
|
-
return None
|
|
66
|
-
forms = lookup.get(norm, [])
|
|
67
|
-
if len(forms) > 1:
|
|
68
|
-
return setmedian(forms)
|
|
69
|
-
for form in forms:
|
|
70
|
-
return str(form)
|
|
71
|
-
return None
|
|
44
|
+
return pick_name(list(values))
|
|
72
45
|
|
|
73
46
|
def _specificity(self, value: str) -> float:
|
|
74
47
|
# TODO: insert artificial intelligence here.
|
|
75
48
|
return dampen(3, 50, value)
|
|
76
49
|
|
|
77
50
|
def compare(self, left: str, right: str) -> float:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
51
|
+
"""Compare two names for similarity."""
|
|
52
|
+
left_clean = clean_name_light(left)
|
|
53
|
+
right_clean = clean_name_light(right)
|
|
54
|
+
if left_clean is None or right_clean is None:
|
|
55
|
+
return 0.0
|
|
56
|
+
return levenshtein_similarity(left_clean, right_clean)
|
|
81
57
|
|
|
82
58
|
def node_id(self, value: str) -> Optional[str]:
|
|
83
59
|
slug = slugify(value)
|
followthemoney/types/phone.py
CHANGED
|
@@ -62,7 +62,9 @@ class PhoneType(PropertyType):
|
|
|
62
62
|
except NumberParseException:
|
|
63
63
|
pass
|
|
64
64
|
|
|
65
|
-
def validate(
|
|
65
|
+
def validate(
|
|
66
|
+
self, value: str, fuzzy: bool = False, format: Optional[str] = None
|
|
67
|
+
) -> bool:
|
|
66
68
|
for num in self._parse_number(value):
|
|
67
69
|
if is_valid_number(num):
|
|
68
70
|
return True
|
followthemoney/types/topic.py
CHANGED
|
@@ -56,7 +56,10 @@ class TopicType(EnumType):
|
|
|
56
56
|
"fin.bank": _("Bank"),
|
|
57
57
|
"fin.fund": _("Fund"),
|
|
58
58
|
"fin.adivsor": _("Financial advisor"),
|
|
59
|
-
"
|
|
59
|
+
"reg.action": _("Regulator action"),
|
|
60
|
+
"reg.warn": _("Regulator warning"),
|
|
61
|
+
"role.pep": _("Politician"),
|
|
62
|
+
"role.pol": _("Non-PEP"),
|
|
60
63
|
"role.rca": _("Close Associate"),
|
|
61
64
|
"role.judge": _("Judge"),
|
|
62
65
|
"role.civil": _("Civil servant"),
|
|
@@ -74,6 +77,7 @@ class TopicType(EnumType):
|
|
|
74
77
|
"asset.frozen": _("Frozen asset"),
|
|
75
78
|
"sanction": _("Sanctioned entity"),
|
|
76
79
|
"sanction.linked": _("Sanction-linked entity"),
|
|
80
|
+
"sanction.counter": _("Counter-sanctioned entity"),
|
|
77
81
|
"export.control": _("Export controlled"),
|
|
78
82
|
"debarment": _("Debarred entity"),
|
|
79
83
|
"poi": _("Person of interest"),
|
followthemoney/types/url.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from typing import Optional, TYPE_CHECKING
|
|
2
|
-
from
|
|
2
|
+
from rigour.urls import clean_url, compare_urls
|
|
3
3
|
|
|
4
4
|
from followthemoney.types.common import PropertyType
|
|
5
5
|
from followthemoney.rdf import URIRef, Identifier
|
|
@@ -33,26 +33,10 @@ class UrlType(PropertyType):
|
|
|
33
33
|
) -> Optional[str]:
|
|
34
34
|
"""Perform intensive care on URLs to make sure they have a scheme
|
|
35
35
|
and a host name. If no scheme is given HTTP is assumed."""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if not len(parsed.netloc):
|
|
41
|
-
if "." in parsed.path and not text.startswith("//"):
|
|
42
|
-
# This is a pretty weird rule meant to catch things like
|
|
43
|
-
# 'www.google.com', but it'll likely backfire in some
|
|
44
|
-
# really creative ways.
|
|
45
|
-
return self.clean_text(f"//{text}")
|
|
46
|
-
return None
|
|
47
|
-
if not len(parsed.scheme):
|
|
48
|
-
parsed = parsed._replace(scheme=self.DEFAULT_SCHEME)
|
|
49
|
-
else:
|
|
50
|
-
parsed = parsed._replace(scheme=parsed.scheme.lower())
|
|
51
|
-
if parsed.scheme not in self.SCHEMES:
|
|
52
|
-
return None
|
|
53
|
-
if not len(parsed.path):
|
|
54
|
-
parsed = parsed._replace(path="/")
|
|
55
|
-
return parsed.geturl()
|
|
36
|
+
return clean_url(text)
|
|
37
|
+
|
|
38
|
+
def compare(self, left: str, right: str) -> float:
|
|
39
|
+
return compare_urls(left, right)
|
|
56
40
|
|
|
57
41
|
def _specificity(self, value: str) -> float:
|
|
58
42
|
return dampen(10, 120, value)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: followthemoney
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.1
|
|
4
4
|
Summary: UNKNOWN
|
|
5
5
|
Home-page: https://followthemoney.tech/
|
|
6
6
|
Author: Organized Crime and Corruption Reporting Project
|
|
@@ -12,27 +12,26 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Classifier: Programming Language :: Python
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.9
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
-
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: babel <3.0.0,>=2.14.0
|
|
16
17
|
Requires-Dist: pyyaml <7.0.0,>=5.0.0
|
|
17
18
|
Requires-Dist: types-PyYAML
|
|
18
19
|
Requires-Dist: sqlalchemy2-stubs
|
|
19
20
|
Requires-Dist: banal <1.1.0,>=1.0.6
|
|
21
|
+
Requires-Dist: rigour <1.0.0,>=0.5.1
|
|
20
22
|
Requires-Dist: click <9.0.0,>=8.0
|
|
21
23
|
Requires-Dist: stringcase <2.0.0,>=1.2.0
|
|
22
24
|
Requires-Dist: requests <3.0.0,>=2.21.0
|
|
23
|
-
Requires-Dist: python-levenshtein <1.0.0,>=0.12.0
|
|
24
25
|
Requires-Dist: normality <3.0.0,>=2.4.0
|
|
25
26
|
Requires-Dist: sqlalchemy <3.0.0,>=1.4.49
|
|
26
27
|
Requires-Dist: countrynames <2.0.0,>=1.13.0
|
|
27
|
-
Requires-Dist: languagecodes <2.0.0,>=1.1.0
|
|
28
28
|
Requires-Dist: prefixdate <1.0.0,>=0.4.0
|
|
29
29
|
Requires-Dist: fingerprints <2.0.0,>=1.0.1
|
|
30
30
|
Requires-Dist: phonenumbers <9.0.0,>=8.12.22
|
|
31
31
|
Requires-Dist: python-stdnum <2.0.0,>=1.16
|
|
32
|
-
Requires-Dist: pantomime <1.0.0,>=0.5.1
|
|
33
32
|
Requires-Dist: pytz >=2021.1
|
|
34
33
|
Requires-Dist: rdflib <7.1.0,>=6.2.0
|
|
35
|
-
Requires-Dist: networkx <3.
|
|
34
|
+
Requires-Dist: networkx <3.4,>=2.5
|
|
36
35
|
Requires-Dist: openpyxl <4.0.0,>=3.0.5
|
|
37
36
|
Requires-Dist: orjson <4.0,>=3.7
|
|
38
37
|
Provides-Extra: dev
|
|
@@ -46,6 +45,7 @@ Requires-Dist: pytest-cov ; extra == 'dev'
|
|
|
46
45
|
Requires-Dist: types-PyYAML ; extra == 'dev'
|
|
47
46
|
Requires-Dist: types-requests ; extra == 'dev'
|
|
48
47
|
Requires-Dist: types-setuptools ; extra == 'dev'
|
|
48
|
+
Requires-Dist: types-openpyxl ; extra == 'dev'
|
|
49
49
|
Requires-Dist: flake8 >=2.6.0 ; extra == 'dev'
|
|
50
50
|
Requires-Dist: transifex-client ; extra == 'dev'
|
|
51
51
|
Requires-Dist: responses >=0.9.0 ; extra == 'dev'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
followthemoney/__init__.py,sha256=
|
|
1
|
+
followthemoney/__init__.py,sha256=WVOrr3U8o6llMvfuRXimFUw-T0ajT6bE05TqR8eQhfg,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
|
|
@@ -8,15 +8,15 @@ followthemoney/model.py,sha256=p4Bk0XPi9z7SKZVgG1FqeM2i45xwAlDIkHwyyUcDKk0,6426
|
|
|
8
8
|
followthemoney/namespace.py,sha256=qYplxKn5OO3zDMf3NItKwTwDsJOnski5JbeyhssqhR8,4434
|
|
9
9
|
followthemoney/offshore.py,sha256=Pf0tx-7GyhIZRueshDRqPNlxkHfGstmW5yNDID5Mnws,1060
|
|
10
10
|
followthemoney/ontology.py,sha256=7PEoUKISNpkRvVhuLeE3IE9ZiNtdR8mn9_kzZ9yF9l0,2986
|
|
11
|
-
followthemoney/property.py,sha256=
|
|
12
|
-
followthemoney/proxy.py,sha256=
|
|
11
|
+
followthemoney/property.py,sha256=EXU4kH6rOD9ubmuDXE-3LGc6gwQspW9_NjSacDb4xYA,7494
|
|
12
|
+
followthemoney/proxy.py,sha256=YvnAUWtu_BCsK36N6xTcoI0Ffjn-gIyj-eDVhtsjoLA,20033
|
|
13
13
|
followthemoney/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
followthemoney/rdf.py,sha256=_BzWiBc61UYjiyadvGKya4P4JlJfvZtNrn8SP2hC2JM,328
|
|
15
15
|
followthemoney/schema.py,sha256=Tk61vRmWaawW-1HOmu-hbYZtkD8it0fIvNtSRxBEQxA,16774
|
|
16
16
|
followthemoney/util.py,sha256=ec_n8Q_CVAfN4HkUnOZB6IRbMz-p4XXxxwTm1RzLYV4,4508
|
|
17
17
|
followthemoney/cli/__init__.py,sha256=Fl05wMr5-FlOSMRpmu1HJSNfPRpy8u9as5IRbGXdo4U,421
|
|
18
18
|
followthemoney/cli/aggregate.py,sha256=Gwfi5Bt1LCwqbpsCu4P1Cr-QJtCWhbaqgGEzfwJUUL4,2142
|
|
19
|
-
followthemoney/cli/cli.py,sha256=
|
|
19
|
+
followthemoney/cli/cli.py,sha256=yrPw2iyKY-E-uRWe6KN9W3ayvz-22vfpe_ZeD0RiI0c,3591
|
|
20
20
|
followthemoney/cli/exports.py,sha256=HsTyIOz1KQSeObp9-9SKzSUBW158XOhpU5_Stv_2HWM,4016
|
|
21
21
|
followthemoney/cli/mapping.py,sha256=aEl57en0zu57yMA2AU5y01U5Yyqo_42hkMlAdfIQP08,3284
|
|
22
22
|
followthemoney/cli/sieve.py,sha256=Wh1UQxzyM9Gh60ooS4s4ydlW1b69bMzFM08tg8ttSIY,1940
|
|
@@ -24,26 +24,26 @@ followthemoney/cli/util.py,sha256=CFcS-PEwpMasMWX_Yg283O_PaAhcPwkvahFNWc13C8c,47
|
|
|
24
24
|
followthemoney/export/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
followthemoney/export/common.py,sha256=_YrXrwsqmyboDZDhtJ_PazUUJYe1Y-Trqc9lz4YlVR8,991
|
|
26
26
|
followthemoney/export/csv.py,sha256=WPAeHMEKnQE1ZL1_I5qgxki-qKJ5gytur-sTOHAubW0,2635
|
|
27
|
-
followthemoney/export/excel.py,sha256
|
|
27
|
+
followthemoney/export/excel.py,sha256=pj6zNpIbye_Zm3vhCamcqHEe9Fw-RyjtWQDCFY6608s,2645
|
|
28
28
|
followthemoney/export/graph.py,sha256=v0z1FgadyFk5aQ0A5q8E9R4fSO-Tpi5JU9YTDwnRKD8,2765
|
|
29
29
|
followthemoney/export/neo4j.py,sha256=JCLb1eW5FFIJXqmnpNMRzRs1KYuYkCtHZp7KJcu-JNY,7104
|
|
30
30
|
followthemoney/export/rdf.py,sha256=E6RiW7oIsJdaBaLAVm6o-MTokARZtqONPuayILqTqo0,786
|
|
31
31
|
followthemoney/mapping/__init__.py,sha256=iwNqzzvrzJNbNDlOCaDLlBTUrNTlnYHIB5cvo_-9oN4,82
|
|
32
32
|
followthemoney/mapping/csv.py,sha256=Tvc6VSh7-ca63LEE4G0yqOCeGMETkuKzUjIkVn4_d7Q,3185
|
|
33
|
-
followthemoney/mapping/entity.py,sha256=
|
|
34
|
-
followthemoney/mapping/property.py,sha256=
|
|
33
|
+
followthemoney/mapping/entity.py,sha256=1Mj6qKkFN10IdElRmULZoqTWIAJ9OSk9wpPpmWBlvA0,5697
|
|
34
|
+
followthemoney/mapping/property.py,sha256=41V16HJh6da7oKdSJWyRcyMkx2XFd6iDm9-4PH7Wihw,5036
|
|
35
35
|
followthemoney/mapping/query.py,sha256=8M6bOlEX2p_bbVwEwTu_1slEtU0cfRJB7ajZp-F07CE,2622
|
|
36
36
|
followthemoney/mapping/source.py,sha256=sri-XpSjeHZbQtqNcz1eJYvwVSBNqGO5JwhWswiEx3c,649
|
|
37
|
-
followthemoney/mapping/sql.py,sha256=
|
|
37
|
+
followthemoney/mapping/sql.py,sha256=m3Ho8B2pFsg6q2zj-y55h0O3x9eb6wzjlPBQyZ6DVcI,4752
|
|
38
38
|
followthemoney/schema/Address.yaml,sha256=xCKyak0qdJC0W5jfROUq6pEMCcSupNSB6GAxx5QfN5E,1689
|
|
39
39
|
followthemoney/schema/Airplane.yaml,sha256=uijTw6-1exomw1clZoDcRvj34mUBmDxNtd6K7nX3XQ4,573
|
|
40
|
-
followthemoney/schema/Analyzable.yaml,sha256=
|
|
40
|
+
followthemoney/schema/Analyzable.yaml,sha256=WvyhjYqI7QzBt3qPDgO7MDasJH1qQtgQMToHkV_xtlU,1237
|
|
41
41
|
followthemoney/schema/Article.yaml,sha256=WI8517oZINSLDmgwJiArWXTjZiK6hY8X3bMlMdwaOn8,260
|
|
42
42
|
followthemoney/schema/Assessment.yaml,sha256=UGxJIIe8xf2ddGWaj0l-ui7KwWwbgRPCEWC3ZFAPrhU,624
|
|
43
43
|
followthemoney/schema/Asset.yaml,sha256=xQhHJJtnGLfiOaUncqGv4JR3Yn7l46yAg7p0hKFAFVE,260
|
|
44
44
|
followthemoney/schema/Associate.yaml,sha256=3u3t5bpDBSQ7p5jyrHTEM0GbhBaG1ar9UKN4uquvq9g,887
|
|
45
45
|
followthemoney/schema/Audio.yaml,sha256=qEoWTw_WrY7EjkztMdRQ-DRA1DZRCreib2HkOlCqKn8,446
|
|
46
|
-
followthemoney/schema/BankAccount.yaml,sha256=
|
|
46
|
+
followthemoney/schema/BankAccount.yaml,sha256=PwQWqEox-MrhBNBRiXLr-P_tJ1qyG3cHtUsUH0QbBZo,1387
|
|
47
47
|
followthemoney/schema/Call.yaml,sha256=kbVCnVxucBrEplxehXHThLSJAJjy_GhWan-IeZZjr0M,980
|
|
48
48
|
followthemoney/schema/CallForTenders.yaml,sha256=2IWonTzfSbrkynMoEWqv5fekUeFM_xDKpKIbRe1XDbo,3227
|
|
49
49
|
followthemoney/schema/Company.yaml,sha256=7dWBPetd7xOJtu8AlhKsjxBYzYBK5E3LssYguSf0glA,3321
|
|
@@ -67,7 +67,7 @@ followthemoney/schema/Identification.yaml,sha256=XVvZXTZZUg8qDzTga8dgF3mJqK9eSDg
|
|
|
67
67
|
followthemoney/schema/Image.yaml,sha256=MMrK4wQJBuN07STwTmsPjXXpYZB7fPt_hQtweSBM4m0,388
|
|
68
68
|
followthemoney/schema/Interest.yaml,sha256=xPYobeuxFMFXUWlQuPvDjAsr3QQDPteRz68_rq7Dvf0,276
|
|
69
69
|
followthemoney/schema/Interval.yaml,sha256=WtmSoBngajhpNLtDZ8Ocpdd0I0tLG-7A4afuHXxssow,1919
|
|
70
|
-
followthemoney/schema/LegalEntity.yaml,sha256=
|
|
70
|
+
followthemoney/schema/LegalEntity.yaml,sha256=p_ty9cXsv8f7TTuz644R5SEmVePW-Gbp0ZrHgCgDIyk,3645
|
|
71
71
|
followthemoney/schema/License.yaml,sha256=9Ye5vGEBhi7ttGqf0DdAGCJCN3zz5HtGu52dCcmCsQk,452
|
|
72
72
|
followthemoney/schema/Membership.yaml,sha256=TzU1A-hPVXRJcNkC78N-EPhZ_qUgO14IZ8lB9WQ63AM,696
|
|
73
73
|
followthemoney/schema/Mention.yaml,sha256=nBeulR_Jm4x75aJ7yNF0TAVhHJqXQaEzOutLIn_YU-4,1086
|
|
@@ -81,7 +81,7 @@ followthemoney/schema/Page.yaml,sha256=sQt_CnVyjDVGVECLQoGYZH4hxpjdPhxVRz4XJW-_1
|
|
|
81
81
|
followthemoney/schema/Pages.yaml,sha256=deUgHFxgC_lmfRon9K6JlCyfJ-5WdoTmLiU_lr393u0,444
|
|
82
82
|
followthemoney/schema/Passport.yaml,sha256=VkGMSJ2rG3_trME4wci45qmk1f_NUHYOn2YoLzrdre0,730
|
|
83
83
|
followthemoney/schema/Payment.yaml,sha256=WRBJuj9ljsxLBs-0g9Z9UD87uR1RTtuUiYnWOnKr1qA,1757
|
|
84
|
-
followthemoney/schema/Person.yaml,sha256=
|
|
84
|
+
followthemoney/schema/Person.yaml,sha256=N4qaVSwDs1V3fzTYTMwg57wOlgbkjaeZuPn0QbYICG8,1886
|
|
85
85
|
followthemoney/schema/PlainText.yaml,sha256=AYShjP5T_YUQypeAGJT7qdXbhBT_KahBKnwoypAqcjQ,272
|
|
86
86
|
followthemoney/schema/Position.yaml,sha256=k5xdmowJfxR-A93aWMqZ1Z3EuEHsTwvXefKyy0K9DtU,1303
|
|
87
87
|
followthemoney/schema/Post.yaml,sha256=NRn3NZpcDUmFW-uUQP8YAYOMYpWirD7-wrljETNh-lA,1029
|
|
@@ -91,12 +91,12 @@ followthemoney/schema/PublicBody.yaml,sha256=QqL_awWLLabrgLo4gUqY99BCmeqz_h16Unr
|
|
|
91
91
|
followthemoney/schema/RealEstate.yaml,sha256=R_KiyvfKRYgmkZUfmo8ANQUJNi-zIfdc34gQhTgn_3A,1357
|
|
92
92
|
followthemoney/schema/Representation.yaml,sha256=gSLx9nc63PgJ8UjR8MpIwzwBm-dvRTgW5vnLPcm4H_0,778
|
|
93
93
|
followthemoney/schema/Sanction.yaml,sha256=3q4Xo8xHU03hjWTDypJe_fVIboSa6D8KOurjxzNGSao,931
|
|
94
|
-
followthemoney/schema/Security.yaml,sha256=
|
|
94
|
+
followthemoney/schema/Security.yaml,sha256=Ix8yCF5sm6JQzitZR7bKb5_P48RNaW1PAF6FDot-ks8,1120
|
|
95
95
|
followthemoney/schema/Similar.yaml,sha256=gD8rZEaPQWzU-rEfsKdn62uEucF3KxYBcPMoSdnxvME,817
|
|
96
96
|
followthemoney/schema/Succession.yaml,sha256=cwz1CwPl8btfUEpmr1YxF6T2WQQuLUboaW9EVEuW2Ng,735
|
|
97
97
|
followthemoney/schema/Table.yaml,sha256=VcgOA2wcczHhRCwObIO39yStoMuoPZYE0VlXrAH9xCQ,635
|
|
98
98
|
followthemoney/schema/TaxRoll.yaml,sha256=5DXK_rkJimpy2ZLjxx2RntYiGPXiK8Iiphr6cJjUiag,740
|
|
99
|
-
followthemoney/schema/Thing.yaml,sha256=
|
|
99
|
+
followthemoney/schema/Thing.yaml,sha256=72gInrI4VEWxUcld7w4by1QPI46F-NR8QZJ09JyUcnc,2438
|
|
100
100
|
followthemoney/schema/Trip.yaml,sha256=nLQD_ApmVJ8D56Czl7K700hhNZjzFV9FOQ3NBSQDLiM,771
|
|
101
101
|
followthemoney/schema/UnknownLink.yaml,sha256=a7BUbNbrsGylewJGg2fcSody_Kmt4eEw_IKIvgHIt8c,668
|
|
102
102
|
followthemoney/schema/UserAccount.yaml,sha256=V1JWwwcggCO4G9ByJY9zlQ0uOVp8HQK2mRXwqaGJnBM,763
|
|
@@ -128,28 +128,28 @@ followthemoney/translations/ru/LC_MESSAGES/followthemoney.mo,sha256=K30zowxjvE3U
|
|
|
128
128
|
followthemoney/translations/ru/LC_MESSAGES/followthemoney.po,sha256=Gva5A2VXKfFboAtQc313CJ1SGNmjhsLZiQCwf-1ZVt8,139453
|
|
129
129
|
followthemoney/translations/tr/LC_MESSAGES/followthemoney.mo,sha256=3h8kg6ryzuV2xmcG6_I0HtS1atddTmIvkeuU3bJcH14,487
|
|
130
130
|
followthemoney/translations/tr/LC_MESSAGES/followthemoney.po,sha256=2yqoZ4vXTpxcSEPwSlF29cXpo3ou4CgaOhIcaalswvA,89829
|
|
131
|
-
followthemoney/types/__init__.py,sha256=
|
|
131
|
+
followthemoney/types/__init__.py,sha256=X9XeM6JktzirAw5gGkyDKGM70NuiJ9Tbjoq0IwVclxU,1745
|
|
132
132
|
followthemoney/types/address.py,sha256=P6ctzojJks70RZIwk5Bhc9djxejZo4Te0m5pQ9M7bAM,1651
|
|
133
133
|
followthemoney/types/checksum.py,sha256=2KLOoZiYUnkButMl_Q8_I4Ju7qfQX8PcdoyzwOf-Aj4,917
|
|
134
|
-
followthemoney/types/common.py,sha256=
|
|
134
|
+
followthemoney/types/common.py,sha256=qPVj-jrDMEASWFYjl02NXoDO6lfPRQxE6dERLt_dmu8,9809
|
|
135
135
|
followthemoney/types/country.py,sha256=1fAwIc8gqmP004Y22uILLJxBD3m552MoDwaEMGOD58o,3053
|
|
136
|
-
followthemoney/types/date.py,sha256=
|
|
137
|
-
followthemoney/types/email.py,sha256=
|
|
138
|
-
followthemoney/types/entity.py,sha256=
|
|
136
|
+
followthemoney/types/date.py,sha256=XWFSKPJLpo8JSfcleyaEXih0q65ydf_d42Y_rcWZ-3s,2663
|
|
137
|
+
followthemoney/types/email.py,sha256=zAnMHwC_FZh7IagpDRhGZf-GfQR6uW8d-lZP4UCdGcM,2745
|
|
138
|
+
followthemoney/types/entity.py,sha256=RCDD9j7MWA_dd0Ib33dl1E6PubsiS1yfVeIOsDxWYpY,2431
|
|
139
139
|
followthemoney/types/gender.py,sha256=N1xghyYtb4WZh8s3ogF9kDTRtx367o3gRZWbG30QGI8,1800
|
|
140
|
-
followthemoney/types/iban.py,sha256=
|
|
141
|
-
followthemoney/types/identifier.py,sha256=
|
|
142
|
-
followthemoney/types/ip.py,sha256=
|
|
140
|
+
followthemoney/types/iban.py,sha256=C8GdfIdDl8Ip89VsZ_hk6v3tJUrRgab_wFLo9ff2lYo,1763
|
|
141
|
+
followthemoney/types/identifier.py,sha256=tX-XepAPreP6u4dzMKyy5mp507NH71DdByYA7pXyKrw,2016
|
|
142
|
+
followthemoney/types/ip.py,sha256=Z4s8mIlY5-IK9Oo13_YbL9lMQpY8SE_NAz0eqaonaYo,1392
|
|
143
143
|
followthemoney/types/json.py,sha256=Hefwns1-ziJf310MWvdfX5ICkOgj9cnnMJuqq1e6qKY,1676
|
|
144
|
-
followthemoney/types/language.py,sha256=
|
|
145
|
-
followthemoney/types/mimetype.py,sha256=
|
|
146
|
-
followthemoney/types/name.py,sha256=
|
|
144
|
+
followthemoney/types/language.py,sha256=4GT8_kqCwq4tOs7s9TffM7SSr9AaszAQsNJHK27fqVY,2612
|
|
145
|
+
followthemoney/types/mimetype.py,sha256=EZ5hIdn-wosfLc-GjXDaOzevxaSXPbSPHDUJmPT1h0I,1355
|
|
146
|
+
followthemoney/types/name.py,sha256=WsVVDIPJ0Hdlll3XvUgRHlpAbtUxu_p-yJmHaEH-4Gc,2093
|
|
147
147
|
followthemoney/types/number.py,sha256=9l5v3hcAoJ4V6jaBS_-Kc1gtdJlQrY9L98ElC5e_PLQ,943
|
|
148
|
-
followthemoney/types/phone.py,sha256=
|
|
148
|
+
followthemoney/types/phone.py,sha256=cppWyfZfOFZoGNjjqpMuTgxSgC1j5JFMpPHMHZYvoDQ,4020
|
|
149
149
|
followthemoney/types/registry.py,sha256=hvQ1oIWz_4PpyUnKA5azbaZCulNb5LCIPyC-AQYVVyw,1936
|
|
150
150
|
followthemoney/types/string.py,sha256=paSCnFt9TJrCmelmw5-OiwCBu0rkqNh6iauteJIj26k,1130
|
|
151
|
-
followthemoney/types/topic.py,sha256=
|
|
152
|
-
followthemoney/types/url.py,sha256=
|
|
151
|
+
followthemoney/types/topic.py,sha256=BuOpyyTKvXoDZ0C6dcHl90Nz64wsmZ8kKLSyBwylHb0,3626
|
|
152
|
+
followthemoney/types/url.py,sha256=6p3ctjh0S_VciPLYuyMPwvK9147rpkhWDd4f2kahe10,1470
|
|
153
153
|
tests/export/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
154
154
|
tests/export/test_csv.py,sha256=r0ACsWnGdLHIZgXvbshGBQ9An0v1dJGAWulRT-Xittc,1261
|
|
155
155
|
tests/export/test_excel.py,sha256=ShLtaJ1-zgq-1rzbNoGJp1bVbRKZ-HcGtU8oJpKwjjY,1696
|
|
@@ -165,22 +165,22 @@ tests/types/test_countries.py,sha256=LiIiDQsrYIFQnD7Z_fwIlLkjyX1p23yVxqdpi3-7tDU
|
|
|
165
165
|
tests/types/test_dates.py,sha256=-S0WOSVPdz93iMgaZ5QCEfe7FOmvFvrL44hcdg9VBDM,3774
|
|
166
166
|
tests/types/test_emails.py,sha256=8CyyedWe--wlbEZ7lQVVLw75pjIcI6ZpeTFUFPqxPg8,1606
|
|
167
167
|
tests/types/test_entity.py,sha256=Jf7LarSILlgdWUV7bmXIQM8yh1YgCkSkrp0rFenduto,819
|
|
168
|
-
tests/types/test_iban.py,sha256=
|
|
169
|
-
tests/types/test_identifiers.py,sha256=
|
|
168
|
+
tests/types/test_iban.py,sha256=4gj9NUPMgKAfB4V3NZpua-c7Q5MSOokQVg4gZGl_SmM,6510
|
|
169
|
+
tests/types/test_identifiers.py,sha256=yP7iv5KdVbNA_iSfVb6MpbpcUAoE2ZJDxmf49lMOFFY,6472
|
|
170
170
|
tests/types/test_ip.py,sha256=lHRqT-MyPWTgtfnWisFm7N5n6TncebDp2ZEiEBIsYBM,1043
|
|
171
171
|
tests/types/test_json.py,sha256=wOTW8wDwfSd2uOok40b7Y2pZKeZnayMVI6wX3hvYjgs,772
|
|
172
172
|
tests/types/test_languages.py,sha256=gWxkmpf48R7Yfk2o41KGkrYrAjA-ew8Rwa4TDHunqmc,947
|
|
173
173
|
tests/types/test_mimetypes.py,sha256=YYIFKJU4DhP6JMyP_sNmtLpsQnOOiZ_rJkIeJ5cgtAk,445
|
|
174
|
-
tests/types/test_names.py,sha256=
|
|
174
|
+
tests/types/test_names.py,sha256=zFgEojnNWbXgkFiB2YdjL1o8-UiMrJxAMaP9XwFVLvY,1060
|
|
175
175
|
tests/types/test_number.py,sha256=uIoKq1aEYuxzRxI0oB30yJnd9oxPJ0r03AWba9AIgFg,474
|
|
176
176
|
tests/types/test_phones.py,sha256=_GTl-0r5hok8ovUJPRqP7k9kJYMVbanjYUMLrbzFb5U,1196
|
|
177
177
|
tests/types/test_registry.py,sha256=IkrHjrEiIIPd4PdMFUg_jVh_gsE6CzGXIF8s3LIbnl0,296
|
|
178
178
|
tests/types/test_topic.py,sha256=m35GSwTy-r77YACGDDac09nYLBIUua42Dy7z292m9EU,707
|
|
179
179
|
tests/types/test_urls.py,sha256=JVmzJ0DKDBle_xLjmOHn0hgDTCpLmljrcfbOM1GSGRw,1170
|
|
180
|
-
followthemoney-3.
|
|
181
|
-
followthemoney-3.
|
|
182
|
-
followthemoney-3.
|
|
183
|
-
followthemoney-3.
|
|
184
|
-
followthemoney-3.
|
|
185
|
-
followthemoney-3.
|
|
186
|
-
followthemoney-3.
|
|
180
|
+
followthemoney-3.6.1.dist-info/LICENSE,sha256=Jln3uF70A9AQySyrP9JAW6sQTubLoKVQunvuJqISv7w,1098
|
|
181
|
+
followthemoney-3.6.1.dist-info/METADATA,sha256=-yLhrfGPKNr-wx54u_K_zOIvWIRDQAeNhMF8ramomLE,4531
|
|
182
|
+
followthemoney-3.6.1.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
|
|
183
|
+
followthemoney-3.6.1.dist-info/entry_points.txt,sha256=ONq-BcCB8Cnk_K1nvWqJPKSRm6sDW0RibLFp0gGtz-k,594
|
|
184
|
+
followthemoney-3.6.1.dist-info/namespace_packages.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
185
|
+
followthemoney-3.6.1.dist-info/top_level.txt,sha256=O33_kGPehy6-69B_k01UgMK55_JOhwqe2de7ND23b-Y,21
|
|
186
|
+
followthemoney-3.6.1.dist-info/RECORD,,
|
tests/types/test_iban.py
CHANGED
|
@@ -15,6 +15,7 @@ class IbansTest(unittest.TestCase):
|
|
|
15
15
|
rdf = ibans.rdf("GB29NWBK60161331926819")
|
|
16
16
|
assert "iban:GB29NWBK60161331926819" in rdf
|
|
17
17
|
nid = ibans.node_id("gb29NWBK60161331926819")
|
|
18
|
+
assert nid is not None
|
|
18
19
|
assert "iban:GB" in nid
|
|
19
20
|
|
|
20
21
|
def test_domain_validity(self):
|
|
@@ -22,7 +23,7 @@ class IbansTest(unittest.TestCase):
|
|
|
22
23
|
self.assertTrue(ibans.validate("GB29NWBK60161331926819"))
|
|
23
24
|
self.assertFalse(ibans.validate("GB28 NWBK 6016 1331 9268 19"))
|
|
24
25
|
self.assertFalse(ibans.validate("GB29NWBKN0161331926819"))
|
|
25
|
-
self.assertFalse(ibans.validate(
|
|
26
|
+
self.assertFalse(ibans.validate(""))
|
|
26
27
|
self.assertTrue(ibans.validate("AL35202111090000000001234567"))
|
|
27
28
|
self.assertTrue(ibans.validate("AD1400080001001234567890"))
|
|
28
29
|
self.assertTrue(ibans.validate("AT483200000012345864"))
|
tests/types/test_identifiers.py
CHANGED
|
@@ -4,6 +4,9 @@ from followthemoney.types import registry
|
|
|
4
4
|
|
|
5
5
|
identifiers = registry.identifier
|
|
6
6
|
|
|
7
|
+
def iban_valid(text: str) -> bool:
|
|
8
|
+
return registry.identifier.validate(text, format='iban')
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
class IdentifiersTest(unittest.TestCase):
|
|
9
12
|
def test_parse(self):
|
|
@@ -21,3 +24,99 @@ class IdentifiersTest(unittest.TestCase):
|
|
|
21
24
|
assert comp > 0, comp
|
|
22
25
|
comp = identifiers.compare_sets(["9818700"], ["AS9818700"])
|
|
23
26
|
assert comp > 0, comp
|
|
27
|
+
iban = "AE460090000000123456789"
|
|
28
|
+
comp = identifiers.compare_safe(iban, iban)
|
|
29
|
+
assert comp == 1, comp
|
|
30
|
+
|
|
31
|
+
def test_iban_parse(self):
|
|
32
|
+
val = "GB29 NWBK 6016 1331 9268 19"
|
|
33
|
+
self.assertEqual(identifiers.clean(val), val)
|
|
34
|
+
self.assertEqual(
|
|
35
|
+
identifiers.clean(val, format='iban'),
|
|
36
|
+
"GB29NWBK60161331926819"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def test_specificity(self):
|
|
40
|
+
self.assertEqual(identifiers.specificity("VG21PACG0000000123456789"), 1)
|
|
41
|
+
|
|
42
|
+
def test_iban_validation(self):
|
|
43
|
+
self.assertTrue(iban_valid("GB29 NWBK 6016 1331 9268 19"))
|
|
44
|
+
self.assertTrue(iban_valid("GB29NWBK60161331926819"))
|
|
45
|
+
self.assertFalse(iban_valid("GB28 NWBK 6016 1331 9268 19"))
|
|
46
|
+
self.assertFalse(iban_valid("GB29NWBKN0161331926819"))
|
|
47
|
+
self.assertFalse(iban_valid(""))
|
|
48
|
+
self.assertTrue(iban_valid("AL35202111090000000001234567"))
|
|
49
|
+
self.assertTrue(iban_valid("AD1400080001001234567890"))
|
|
50
|
+
self.assertTrue(iban_valid("AT483200000012345864"))
|
|
51
|
+
self.assertTrue(iban_valid("AZ96AZEJ00000000001234567890"))
|
|
52
|
+
self.assertTrue(iban_valid("BH02CITI00001077181611"))
|
|
53
|
+
# self.assertTrue(iban_valid('BY86AKBB10100000002966000000'))
|
|
54
|
+
self.assertTrue(iban_valid("BE71096123456769"))
|
|
55
|
+
self.assertTrue(iban_valid("BA275680000123456789"))
|
|
56
|
+
self.assertTrue(iban_valid("BR1500000000000010932840814P2"))
|
|
57
|
+
self.assertTrue(iban_valid("BG18RZBB91550123456789"))
|
|
58
|
+
# self.assertTrue(iban_valid('CR37012600000123456789'))
|
|
59
|
+
self.assertTrue(iban_valid("HR1723600001101234565"))
|
|
60
|
+
self.assertTrue(iban_valid("CY21002001950000357001234567"))
|
|
61
|
+
self.assertTrue(iban_valid("CZ5508000000001234567899"))
|
|
62
|
+
self.assertTrue(iban_valid("DK9520000123456789"))
|
|
63
|
+
self.assertTrue(iban_valid("DO22ACAU00000000000123456789"))
|
|
64
|
+
# self.assertTrue(iban_valid('SV43ACAT00000000000000123123'))
|
|
65
|
+
self.assertTrue(iban_valid("EE471000001020145685"))
|
|
66
|
+
self.assertTrue(iban_valid("FO9264600123456789"))
|
|
67
|
+
self.assertTrue(iban_valid("FI1410093000123458"))
|
|
68
|
+
self.assertTrue(iban_valid("FR7630006000011234567890189"))
|
|
69
|
+
self.assertTrue(iban_valid("GE60NB0000000123456789"))
|
|
70
|
+
self.assertTrue(iban_valid("DE91100000000123456789"))
|
|
71
|
+
self.assertTrue(iban_valid("GI04BARC000001234567890"))
|
|
72
|
+
self.assertTrue(iban_valid("GR9608100010000001234567890"))
|
|
73
|
+
self.assertTrue(iban_valid("GL8964710123456789"))
|
|
74
|
+
self.assertTrue(iban_valid("GT20AGRO00000000001234567890"))
|
|
75
|
+
self.assertTrue(iban_valid("HU93116000060000000012345676"))
|
|
76
|
+
self.assertTrue(iban_valid("IS030001121234561234567890"))
|
|
77
|
+
# self.assertTrue(iban_valid('IQ20CBIQ861800101010500'))
|
|
78
|
+
self.assertTrue(iban_valid("IE64IRCE92050112345678"))
|
|
79
|
+
self.assertTrue(iban_valid("IL170108000000012612345"))
|
|
80
|
+
self.assertTrue(iban_valid("IT60X0542811101000000123456"))
|
|
81
|
+
self.assertTrue(iban_valid("JO71CBJO0000000000001234567890"))
|
|
82
|
+
self.assertTrue(iban_valid("KZ563190000012344567"))
|
|
83
|
+
self.assertTrue(iban_valid("XK051212012345678906"))
|
|
84
|
+
self.assertTrue(iban_valid("KW81CBKU0000000000001234560101"))
|
|
85
|
+
self.assertTrue(iban_valid("LV97HABA0012345678910"))
|
|
86
|
+
self.assertTrue(iban_valid("LB92000700000000123123456123"))
|
|
87
|
+
self.assertTrue(iban_valid("LI7408806123456789012"))
|
|
88
|
+
self.assertTrue(iban_valid("LT601010012345678901"))
|
|
89
|
+
self.assertTrue(iban_valid("LU120010001234567891"))
|
|
90
|
+
self.assertTrue(iban_valid("MK07200002785123453"))
|
|
91
|
+
self.assertTrue(iban_valid("MT31MALT01100000000000000000123"))
|
|
92
|
+
self.assertTrue(iban_valid("MR1300020001010000123456753"))
|
|
93
|
+
self.assertTrue(iban_valid("MU43BOMM0101123456789101000MUR"))
|
|
94
|
+
self.assertTrue(iban_valid("MD21EX000000000001234567"))
|
|
95
|
+
self.assertTrue(iban_valid("MC5810096180790123456789085"))
|
|
96
|
+
self.assertTrue(iban_valid("ME25505000012345678951"))
|
|
97
|
+
self.assertTrue(iban_valid("NL02ABNA0123456789"))
|
|
98
|
+
self.assertTrue(iban_valid("NO8330001234567"))
|
|
99
|
+
self.assertTrue(iban_valid("PK36SCBL0000001123456702"))
|
|
100
|
+
self.assertTrue(iban_valid("PS92PALS000000000400123456702"))
|
|
101
|
+
self.assertTrue(iban_valid("PL10105000997603123456789123"))
|
|
102
|
+
self.assertTrue(iban_valid("PT50002700000001234567833"))
|
|
103
|
+
self.assertTrue(iban_valid("QA54QNBA000000000000693123456"))
|
|
104
|
+
self.assertTrue(iban_valid("RO09BCYP0000001234567890"))
|
|
105
|
+
self.assertTrue(iban_valid("LC14BOSL123456789012345678901234"))
|
|
106
|
+
self.assertTrue(iban_valid("SM76P0854009812123456789123"))
|
|
107
|
+
self.assertTrue(iban_valid("ST23000200000289355710148"))
|
|
108
|
+
self.assertTrue(iban_valid("SA4420000001234567891234"))
|
|
109
|
+
self.assertTrue(iban_valid("RS35105008123123123173"))
|
|
110
|
+
self.assertTrue(iban_valid("SC52BAHL01031234567890123456USD"))
|
|
111
|
+
self.assertTrue(iban_valid("SK8975000000000012345671"))
|
|
112
|
+
self.assertTrue(iban_valid("SI56192001234567892"))
|
|
113
|
+
self.assertTrue(iban_valid("ES7921000813610123456789"))
|
|
114
|
+
self.assertTrue(iban_valid("SE1412345678901234567890"))
|
|
115
|
+
self.assertTrue(iban_valid("CH5604835012345678009"))
|
|
116
|
+
self.assertTrue(iban_valid("TL380080012345678910157"))
|
|
117
|
+
self.assertTrue(iban_valid("TN4401000067123456789123"))
|
|
118
|
+
self.assertTrue(iban_valid("TR320010009999901234567890"))
|
|
119
|
+
self.assertTrue(iban_valid("UA903052992990004149123456789"))
|
|
120
|
+
self.assertTrue(iban_valid("AE460090000000123456789"))
|
|
121
|
+
self.assertTrue(iban_valid("GB98MIDL07009312345678"))
|
|
122
|
+
self.assertTrue(iban_valid("VG21PACG0000000123456789"))
|
tests/types/test_names.py
CHANGED
|
@@ -20,13 +20,7 @@ class NamesTest(unittest.TestCase):
|
|
|
20
20
|
self.assertEqual(names.pick(["Banana"]), "Banana")
|
|
21
21
|
self.assertEqual(names.pick([]), None)
|
|
22
22
|
values = ["Robert Smith", "Rob Smith", "Robert SMITH"]
|
|
23
|
-
self.assertEqual(names.pick(values), "Robert
|
|
24
|
-
|
|
25
|
-
# handle dirty edgecases
|
|
26
|
-
values = ["", "(", "Peter"]
|
|
27
|
-
self.assertEqual(names.pick(values), "Peter")
|
|
28
|
-
values = ["", "("]
|
|
29
|
-
self.assertEqual(names.pick(values), None)
|
|
23
|
+
self.assertEqual(names.pick(values), "Robert Smith")
|
|
30
24
|
|
|
31
25
|
def test_domain_validity(self):
|
|
32
26
|
self.assertTrue(names.validate("huhu"))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|