followthemoney 3.5.9__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.
Files changed (38) hide show
  1. followthemoney/__init__.py +1 -1
  2. followthemoney/cli/cli.py +18 -14
  3. followthemoney/export/excel.py +6 -6
  4. followthemoney/mapping/entity.py +14 -2
  5. followthemoney/mapping/property.py +15 -3
  6. followthemoney/mapping/sql.py +1 -1
  7. followthemoney/property.py +11 -0
  8. followthemoney/proxy.py +29 -19
  9. followthemoney/schema/Analyzable.yaml +2 -0
  10. followthemoney/schema/BankAccount.yaml +3 -0
  11. followthemoney/schema/LegalEntity.yaml +4 -0
  12. followthemoney/schema/Person.yaml +4 -0
  13. followthemoney/schema/Security.yaml +2 -0
  14. followthemoney/schema/Thing.yaml +1 -0
  15. followthemoney/types/__init__.py +2 -2
  16. followthemoney/types/common.py +13 -5
  17. followthemoney/types/date.py +7 -2
  18. followthemoney/types/email.py +3 -1
  19. followthemoney/types/entity.py +3 -1
  20. followthemoney/types/iban.py +7 -9
  21. followthemoney/types/identifier.py +17 -0
  22. followthemoney/types/ip.py +3 -1
  23. followthemoney/types/language.py +1 -1
  24. followthemoney/types/mimetype.py +2 -2
  25. followthemoney/types/name.py +6 -35
  26. followthemoney/types/phone.py +3 -1
  27. followthemoney/types/topic.py +5 -1
  28. followthemoney/types/url.py +5 -21
  29. {followthemoney-3.5.9.dist-info → followthemoney-3.6.0.dist-info}/METADATA +6 -10
  30. {followthemoney-3.5.9.dist-info → followthemoney-3.6.0.dist-info}/RECORD +38 -38
  31. {followthemoney-3.5.9.dist-info → followthemoney-3.6.0.dist-info}/WHEEL +1 -1
  32. {followthemoney-3.5.9.dist-info → followthemoney-3.6.0.dist-info}/entry_points.txt +0 -1
  33. tests/types/test_iban.py +2 -1
  34. tests/types/test_identifiers.py +99 -0
  35. tests/types/test_names.py +1 -7
  36. {followthemoney-3.5.9.dist-info → followthemoney-3.6.0.dist-info}/LICENSE +0 -0
  37. {followthemoney-3.5.9.dist-info → followthemoney-3.6.0.dist-info}/namespace_packages.txt +0 -0
  38. {followthemoney-3.5.9.dist-info → followthemoney-3.6.0.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,7 @@ import os
3
3
  from followthemoney.model import Model
4
4
  from followthemoney.util import set_model_locale
5
5
 
6
- __version__ = "3.5.9"
6
+ __version__ = "3.6.0"
7
7
 
8
8
 
9
9
  model_path = os.path.dirname(__file__)
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, TextIO
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, read_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("w"), default="-")
24
- def dump_model(outfile: TextIO) -> None:
25
- outfile.write(json.dumps(model.to_dict(), indent=2, sort_keys=True))
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 (prop, value) in entity.itervalues():
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, "r") as infh:
50
- data = json.load(infh)
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
- if "layout" in data:
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.get_text_stream("stdout")
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 = json.dumps(entity.to_dict(), indent=2)
82
- stdout.write(data + "\n")
85
+ data = orjson.dumps(entity.to_dict(), option=f)
86
+ stdout.write(data)
83
87
  except BrokenPipeError:
84
88
  raise click.Abort()
@@ -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 # type: ignore
5
- from openpyxl.cell import WriteOnlyCell # type: ignore
6
- from openpyxl.styles import Font, PatternFill # type: ignore
7
- from openpyxl.worksheet.worksheet import Worksheet # type: ignore
8
- from openpyxl.utils.exceptions import IllegalCharacterError # type: ignore
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 = []
@@ -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
- ) -> None:
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 None
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(self.prop, value, fuzzy=self.fuzzy, format=self.format)
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
@@ -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")
@@ -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
- ) -> None:
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
- if value is not None:
215
- # Somewhat hacky: limit the maximum size of any particular
216
- # field to avoid overloading upstream aleph/elasticsearch.
217
- value_size = len(value)
218
- if prop.type.max_size is not None:
219
- if self._size + value_size > prop.type.max_size:
220
- # msg = "[%s] too large. Rejecting additional values."
221
- # log.warning(msg, prop.name)
222
- return None
223
- self._size += value_size
224
- self._properties.setdefault(prop.name, list())
225
- if value not in self._properties[prop.name]:
226
- self._properties[prop.name].append(value)
227
- return None
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
- data.update(
428
- {"id": self.id, "schema": self.schema.name, "properties": self.properties}
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]:
@@ -32,6 +32,8 @@ Analyzable:
32
32
  ibanMentioned:
33
33
  label: "Detected IBANs"
34
34
  hidden: true
35
+ # type: identifier
36
+ # format: iban
35
37
  type: iban
36
38
  ipMentioned:
37
39
  label: "Detected IP addresses"
@@ -28,10 +28,13 @@ BankAccount:
28
28
  type: identifier
29
29
  iban:
30
30
  label: IBAN
31
+ # type: identifier
32
+ # format: iban
31
33
  type: iban
32
34
  bic:
33
35
  label: Bank Identifier Code
34
36
  type: identifier
37
+ format: bic
35
38
  bank:
36
39
  label: Bank
37
40
  type: entity
@@ -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
@@ -57,6 +57,7 @@ Thing:
57
57
  wikidataId:
58
58
  label: Wikidata ID
59
59
  type: identifier
60
+ format: qid
60
61
  keywords:
61
62
  label: Keywords
62
63
  topics:
@@ -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)
@@ -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
- return self.__doc__
68
+ if not self.__doc__:
69
+ return None
70
+
71
+ return cleandoc(self.__doc__)
68
72
 
69
- def validate(self, value: str) -> bool:
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 (l, r) in product(ensure_list(left), ensure_list(right)):
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(self, value: str) -> bool:
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
@@ -27,9 +27,14 @@ class DateType(PropertyType):
27
27
  plural = _("Dates")
28
28
  matchable = True
29
29
 
30
- def validate(self, value: str) -> bool:
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
- prefix = parse(value)
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(
@@ -36,7 +36,9 @@ class EmailType(PropertyType):
36
36
  # except:
37
37
  # return False
38
38
 
39
- def validate(self, value: str) -> bool:
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)
@@ -29,7 +29,9 @@ class EntityType(PropertyType):
29
29
  matchable = True
30
30
  pivot = True
31
31
 
32
- def validate(self, value: str) -> bool:
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
@@ -1,6 +1,5 @@
1
- from typing import Optional, TYPE_CHECKING, cast
2
- from stdnum import iban # type: ignore
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
- try:
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 text.replace(" ", "").upper()
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 cast(str, iban.format(value))
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)
@@ -21,7 +21,9 @@ class IpType(PropertyType):
21
21
  matchable = True
22
22
  pivot = True
23
23
 
24
- def validate(self, value: str) -> bool:
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)
@@ -1,6 +1,6 @@
1
1
  from typing import Optional, TYPE_CHECKING
2
2
  from babel.core import Locale
3
- from languagecodes import iso_639_alpha3
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
@@ -1,6 +1,6 @@
1
1
  from typing import Optional, TYPE_CHECKING
2
- from pantomime import normalize_mimetype, parse_mimetype
3
- from pantomime import DEFAULT
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
@@ -1,6 +1,6 @@
1
- from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Union
2
-
3
- from Levenshtein import distance, setmedian
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
6
 
@@ -40,44 +40,15 @@ class NameType(PropertyType):
40
40
 
41
41
  def pick(self, values: Sequence[str]) -> Optional[str]:
42
42
  """From a set of names, pick the most plausible user-facing one."""
43
- # Sort to get stable results when it's a coin toss:
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
43
+ return pick_name(list(values))
72
44
 
73
45
  def _specificity(self, value: str) -> float:
74
46
  # TODO: insert artificial intelligence here.
75
47
  return dampen(3, 50, value)
76
48
 
77
49
  def compare(self, left: str, right: str) -> float:
78
- longest = float(max(len(left), len(right), 1))
79
- edits = float(distance(left[:255], right[:255]))
80
- return (longest - edits) / longest
50
+ """Compare two names for similarity."""
51
+ return levenshtein_similarity(left, right)
81
52
 
82
53
  def node_id(self, value: str) -> Optional[str]:
83
54
  slug = slugify(value)
@@ -62,7 +62,9 @@ class PhoneType(PropertyType):
62
62
  except NumberParseException:
63
63
  pass
64
64
 
65
- def validate(self, value: str) -> bool:
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
@@ -56,7 +56,10 @@ class TopicType(EnumType):
56
56
  "fin.bank": _("Bank"),
57
57
  "fin.fund": _("Fund"),
58
58
  "fin.adivsor": _("Financial advisor"),
59
- "role.pep": _("Political"),
59
+ "reg.action": _("Regulator action"),
60
+ "reg.warn": _("Regulator warning"),
61
+ "role.pep": _("Politican"),
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"),
@@ -1,5 +1,5 @@
1
1
  from typing import Optional, TYPE_CHECKING
2
- from urllib.parse import urlparse
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
- try:
37
- parsed = urlparse(text)
38
- except (TypeError, ValueError):
39
- return None
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,38 +1,35 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: followthemoney
3
- Version: 3.5.9
4
- Summary: UNKNOWN
3
+ Version: 3.6.0
5
4
  Home-page: https://followthemoney.tech/
6
5
  Author: Organized Crime and Corruption Reporting Project
7
6
  Author-email: data@occrp.org
8
7
  License: MIT
9
- Platform: UNKNOWN
10
8
  Classifier: Intended Audience :: Developers
11
9
  Classifier: Operating System :: OS Independent
12
10
  Classifier: Programming Language :: Python
13
11
  Classifier: Programming Language :: Python :: 3.9
14
12
  Description-Content-Type: text/markdown
15
- Requires-Dist: babel <3.0.0,>=2.9.1
13
+ License-File: LICENSE
14
+ Requires-Dist: babel <3.0.0,>=2.14.0
16
15
  Requires-Dist: pyyaml <7.0.0,>=5.0.0
17
16
  Requires-Dist: types-PyYAML
18
17
  Requires-Dist: sqlalchemy2-stubs
19
18
  Requires-Dist: banal <1.1.0,>=1.0.6
19
+ Requires-Dist: rigour <1.0.0,>=0.5.1
20
20
  Requires-Dist: click <9.0.0,>=8.0
21
21
  Requires-Dist: stringcase <2.0.0,>=1.2.0
22
22
  Requires-Dist: requests <3.0.0,>=2.21.0
23
- Requires-Dist: python-levenshtein <1.0.0,>=0.12.0
24
23
  Requires-Dist: normality <3.0.0,>=2.4.0
25
24
  Requires-Dist: sqlalchemy <3.0.0,>=1.4.49
26
25
  Requires-Dist: countrynames <2.0.0,>=1.13.0
27
- Requires-Dist: languagecodes <2.0.0,>=1.1.0
28
26
  Requires-Dist: prefixdate <1.0.0,>=0.4.0
29
27
  Requires-Dist: fingerprints <2.0.0,>=1.0.1
30
28
  Requires-Dist: phonenumbers <9.0.0,>=8.12.22
31
29
  Requires-Dist: python-stdnum <2.0.0,>=1.16
32
- Requires-Dist: pantomime <1.0.0,>=0.5.1
33
30
  Requires-Dist: pytz >=2021.1
34
31
  Requires-Dist: rdflib <7.1.0,>=6.2.0
35
- Requires-Dist: networkx <3.3,>=2.5
32
+ Requires-Dist: networkx <3.4,>=2.5
36
33
  Requires-Dist: openpyxl <4.0.0,>=3.0.5
37
34
  Requires-Dist: orjson <4.0,>=3.7
38
35
  Provides-Extra: dev
@@ -46,6 +43,7 @@ Requires-Dist: pytest-cov ; extra == 'dev'
46
43
  Requires-Dist: types-PyYAML ; extra == 'dev'
47
44
  Requires-Dist: types-requests ; extra == 'dev'
48
45
  Requires-Dist: types-setuptools ; extra == 'dev'
46
+ Requires-Dist: types-openpyxl ; extra == 'dev'
49
47
  Requires-Dist: flake8 >=2.6.0 ; extra == 'dev'
50
48
  Requires-Dist: transifex-client ; extra == 'dev'
51
49
  Requires-Dist: responses >=0.9.0 ; extra == 'dev'
@@ -127,5 +125,3 @@ or `major` arguments.
127
125
  When the schema is updated, please update the docs, ideally including the
128
126
  diagrams. For the RDF namespace and JavaScript version of the model,
129
127
  run `make generate`.
130
-
131
-
@@ -1,4 +1,4 @@
1
- followthemoney/__init__.py,sha256=lD-aHuRdiV82zFlHH4I7iQDZHJ7J3pBrARbNi9I0PpY,360
1
+ followthemoney/__init__.py,sha256=r3yXZluiZbP17mu2VZIJ5Q9_ItUb_kX81WIMtyWzaK0,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=H5VEH4ZE6pAjqxhx4QoWUEKq2w2Pyx9RX5Qh0W9AUmU,7052
12
- followthemoney/proxy.py,sha256=cLMEGTRaliz0dMUYLQNC9jgGMTgsiT5iaEN9t5MNGzQ,19871
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=0ie811Mrk6AbfKMUjP2Z_QdleRWTs360U9lxKKpfsBw,3345
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=-pj4GYIDtnqxSMV5WwWchLwb24UPcQ2cKvyTh4_ARKA,2714
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=1t9AkeMwX5zaKFC1ljEVXZkWHni7hTFXcw1-5u3ZZko,4785
34
- followthemoney/mapping/property.py,sha256=L6hOeDTx_zJRCNVFBuJesPgNFiBdaW2N3UNjclfB3Ss,4748
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=1s0qnEHP8KXjGLrrdcqX_Tb-r59MS-dcNB2ky0Eeves,4754
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=KplYTaca-CDGkg5TSrxZ7aGlSaI9ThTRKBdFkNk6OVQ,1191
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=5hyvQi4VODjmxqEkexJjBT7QqTGM3iuRo5nczZWZYlg,1323
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=I-Wej5ksDxbC13p11YpH_tuMuufPZNERYjgoiPO2wtU,3568
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=Ucqh9RHbgdpHCLoqcN6xcY-yUShAF8HLI4QI2LKXWFc,1586
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=o17-r2Z9Uq4L7-6u9BO8k4XIXCXp5E1SMI0fyr7IJ6E,1082
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=uNfrr-49DcBVHI0KTqjMIONHnBruYXCiGLE3m_hAqBU,2420
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=OeC6w66C3GP6e_XnKwmfV1xPiX2CAg47rXPSE-5eI6Q,1729
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=ZsZMeLLjSmnqLzilP9yBy5FeFQcbMWGrOVn17-bGfgg,9560
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=Q-WbAT9Frddrc2nesj18sYqtx6n8LlWm0DzH-vUWMFg,2500
137
- followthemoney/types/email.py,sha256=WEh5a6BkpGf0xazS0b-g2Ok81BJlF8sN-sTctXEk07Q,2680
138
- followthemoney/types/entity.py,sha256=vIJHg7_GUtYaIGDzrUg8CmRzQFoNeYiHF8jl3PvKzF4,2366
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=tClGDKF2SkNVFy-wT0ngDshvV0TFBO6Z7Rzzg1H15Lo,1848
141
- followthemoney/types/identifier.py,sha256=fj2mxBjG4UTvSElRYI1IXLmt4AilERG5oDm9qp9dW4w,1472
142
- followthemoney/types/ip.py,sha256=Td14wbIxFJKqWf3IiQToKnUAywZG3C4J7NhefcxcLxQ,1327
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=O91yohcv76VJ2FVCY6KuHslfiAlhPERnqhk8MHyGOoQ,2613
145
- followthemoney/types/mimetype.py,sha256=kcPzPmO_CJPFR96R42YtmelOdRj1nqI2_sdDitftzVw,1351
146
- followthemoney/types/name.py,sha256=WlTb-wwnsd4-I9hyKm3d38sEf6tBa6_Yin4UaLWh_AI,2855
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=EbdHQz08qXYJk2DRqtQ4cbYasDuPHWAVziJ-MyrsBLI,1864
147
147
  followthemoney/types/number.py,sha256=9l5v3hcAoJ4V6jaBS_-Kc1gtdJlQrY9L98ElC5e_PLQ,943
148
- followthemoney/types/phone.py,sha256=rbBjj7a5cjGuc6QXy3VwsHbAtIdfkd5CrUkBvBF9Rqg,3955
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=VEvAcJh7uyBwRVUM7P5i0g8WhkcnTEmlj5QwYx50p1M,3442
152
- followthemoney/types/url.py,sha256=WzPjBZe6BnE1Wpj4HotqmeCYijh3iyMcbfErt6saeEU,2162
151
+ followthemoney/types/topic.py,sha256=6kIXdlZNeE28MPbRXaDhSI1whNA7tpkTgFwU_o_gxds,3625
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=7_KybOYh4CbgFfWs8VXoKI8J1qJFoEEE9o629TilzZg,6481
169
- tests/types/test_identifiers.py,sha256=obVGC-0nILxUU33iNevlY9I_YDKmy2n1LcxHl583WT0,725
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=S4QUZhbvnqhX397QNAZLcl6sd1Wg5zF1TfqV23k1j-s,1262
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.5.9.dist-info/LICENSE,sha256=Jln3uF70A9AQySyrP9JAW6sQTubLoKVQunvuJqISv7w,1098
181
- followthemoney-3.5.9.dist-info/METADATA,sha256=G1s1hNYIusJ-zE1ognjLBfIw_qQdxHN6PHlG7ejBB84,4558
182
- followthemoney-3.5.9.dist-info/WHEEL,sha256=-G_t0oGuE7UD0DrSpVZnq1hHMBV9DD2XkS5v7XpmTnk,110
183
- followthemoney-3.5.9.dist-info/entry_points.txt,sha256=ONq-BcCB8Cnk_K1nvWqJPKSRm6sDW0RibLFp0gGtz-k,594
184
- followthemoney-3.5.9.dist-info/namespace_packages.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
185
- followthemoney-3.5.9.dist-info/top_level.txt,sha256=O33_kGPehy6-69B_k01UgMK55_JOhwqe2de7ND23b-Y,21
186
- followthemoney-3.5.9.dist-info/RECORD,,
180
+ followthemoney-3.6.0.dist-info/LICENSE,sha256=Jln3uF70A9AQySyrP9JAW6sQTubLoKVQunvuJqISv7w,1098
181
+ followthemoney-3.6.0.dist-info/METADATA,sha256=WO2TZ3GZpkAnhwWOT8LYGaQJjbFIJQVEQc1c6HeBxRc,4494
182
+ followthemoney-3.6.0.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
183
+ followthemoney-3.6.0.dist-info/entry_points.txt,sha256=wpgi-5-jyqCy-yw0eYZbnhBuPja_q3RJmiEl7NymXtQ,593
184
+ followthemoney-3.6.0.dist-info/namespace_packages.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
185
+ followthemoney-3.6.0.dist-info/top_level.txt,sha256=O33_kGPehy6-69B_k01UgMK55_JOhwqe2de7ND23b-Y,21
186
+ followthemoney-3.6.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py2-none-any
5
5
  Tag: py3-none-any
@@ -15,4 +15,3 @@ gexf = followthemoney.cli.exports:export_gexf
15
15
  mapping = followthemoney.cli.mapping:run_mapping
16
16
  rdf = followthemoney.cli.exports:export_rdf
17
17
  sieve = followthemoney.cli.sieve:sieve
18
-
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(None))
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"))
@@ -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 SMITH")
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"))