followthemoney 3.5.8__py2.py3-none-any.whl → 3.6.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) 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/translations/ru/LC_MESSAGES/followthemoney.po +24 -22
  16. followthemoney/types/__init__.py +2 -2
  17. followthemoney/types/common.py +13 -5
  18. followthemoney/types/date.py +7 -2
  19. followthemoney/types/email.py +3 -1
  20. followthemoney/types/entity.py +3 -1
  21. followthemoney/types/iban.py +7 -9
  22. followthemoney/types/identifier.py +17 -0
  23. followthemoney/types/ip.py +3 -1
  24. followthemoney/types/language.py +1 -1
  25. followthemoney/types/mimetype.py +2 -2
  26. followthemoney/types/name.py +6 -35
  27. followthemoney/types/phone.py +3 -1
  28. followthemoney/types/topic.py +6 -1
  29. followthemoney/types/url.py +5 -21
  30. {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/METADATA +6 -10
  31. {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/RECORD +40 -40
  32. {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/WHEEL +1 -1
  33. {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/entry_points.txt +0 -1
  34. tests/types/test_dates.py +5 -5
  35. tests/types/test_iban.py +2 -1
  36. tests/types/test_identifiers.py +99 -0
  37. tests/types/test_names.py +1 -7
  38. {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/LICENSE +0 -0
  39. {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/namespace_packages.txt +0 -0
  40. {followthemoney-3.5.8.dist-info → followthemoney-3.6.0.dist-info}/top_level.txt +0 -0
@@ -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
@@ -38,6 +38,7 @@ class TopicType(EnumType):
38
38
  "corp.offshore": _("Offshore"),
39
39
  "corp.shell": _("Shell company"),
40
40
  "corp.public": _("Public listed company"),
41
+ "corp.disqual": _("Disqualified"),
41
42
  "gov": _("Government"),
42
43
  "gov.national": _("National government"),
43
44
  "gov.state": _("State government"),
@@ -55,7 +56,10 @@ class TopicType(EnumType):
55
56
  "fin.bank": _("Bank"),
56
57
  "fin.fund": _("Fund"),
57
58
  "fin.adivsor": _("Financial advisor"),
58
- "role.pep": _("Political"),
59
+ "reg.action": _("Regulator action"),
60
+ "reg.warn": _("Regulator warning"),
61
+ "role.pep": _("Politican"),
62
+ "role.pol": _("Non-PEP"),
59
63
  "role.rca": _("Close Associate"),
60
64
  "role.judge": _("Judge"),
61
65
  "role.civil": _("Civil servant"),
@@ -73,6 +77,7 @@ class TopicType(EnumType):
73
77
  "asset.frozen": _("Frozen asset"),
74
78
  "sanction": _("Sanctioned entity"),
75
79
  "sanction.linked": _("Sanction-linked entity"),
80
+ "sanction.counter": _("Counter-sanctioned entity"),
76
81
  "export.control": _("Export controlled"),
77
82
  "debarment": _("Debarred entity"),
78
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.8
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=NzgzcG3gpV-_WtrV6FsyWybG4lSv2rVMjH5colZypnc,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
@@ -125,31 +125,31 @@ followthemoney/translations/pt_BR/LC_MESSAGES/followthemoney.mo,sha256=wes82jLA_
125
125
  followthemoney/translations/pt_BR/LC_MESSAGES/followthemoney.po,sha256=sSj8Mfu8Oq0m-RF_quETKl4f5zMYtJOYGof4-qff8bE,89847
126
126
  followthemoney/translations/ru/followthemoney.po,sha256=SgrP3BfjgAp8e4Y9htH7Il2tqGuzy8YXoAqzyQl12Ko,130621
127
127
  followthemoney/translations/ru/LC_MESSAGES/followthemoney.mo,sha256=K30zowxjvE3UzoO1eXG8qTTL5XeSPkdwCMTmVDXDG68,64182
128
- followthemoney/translations/ru/LC_MESSAGES/followthemoney.po,sha256=6jjNop-JdZMXzy0BZgv-CfS1mnQaYUztrRbWXWfSsgM,138402
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=ySqS74UAeaa2-5n7fvJFzDEKwreuXIjQtObGO5qQEnk,3399
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
@@ -162,25 +162,25 @@ tests/types/test_addresses.py,sha256=49L67aGIJS8CjwZI1I3DwD_MEtV_ALG-syAIuQHYAbE
162
162
  tests/types/test_checksum.py,sha256=eKJ5enVvS8TdhL8GnhtqhwTwpvih8ndOtnv4zLfNCws,216
163
163
  tests/types/test_common.py,sha256=PCaewAF2x08M4Jv7YEPxaSoSLpci6LzKShOqYIB1xH0,673
164
164
  tests/types/test_countries.py,sha256=LiIiDQsrYIFQnD7Z_fwIlLkjyX1p23yVxqdpi3-7tDU,1132
165
- tests/types/test_dates.py,sha256=77wv_bQUqg2oYq_sB-8sD58_qH-gY146wKcD1O6zkno,3739
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.8.dist-info/LICENSE,sha256=Jln3uF70A9AQySyrP9JAW6sQTubLoKVQunvuJqISv7w,1098
181
- followthemoney-3.5.8.dist-info/METADATA,sha256=AlU33UJS8Oxh-1WH-Jsh5k5Ava4SeG2A7sICDoqwNyM,4558
182
- followthemoney-3.5.8.dist-info/WHEEL,sha256=-G_t0oGuE7UD0DrSpVZnq1hHMBV9DD2XkS5v7XpmTnk,110
183
- followthemoney-3.5.8.dist-info/entry_points.txt,sha256=ONq-BcCB8Cnk_K1nvWqJPKSRm6sDW0RibLFp0gGtz-k,594
184
- followthemoney-3.5.8.dist-info/namespace_packages.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
185
- followthemoney-3.5.8.dist-info/top_level.txt,sha256=O33_kGPehy6-69B_k01UgMK55_JOhwqe2de7ND23b-Y,21
186
- followthemoney-3.5.8.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_dates.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import unittest
3
- from datetime import datetime
3
+ from datetime import datetime, timezone
4
4
 
5
5
  from followthemoney.types import registry
6
6
 
@@ -15,7 +15,7 @@ class DatesTest(unittest.TestCase):
15
15
  self.assertTrue(dates.validate("2017-04-04T10:30:29+0200"))
16
16
  self.assertTrue(dates.validate("2017-04-04T10:30:29+03:00"))
17
17
  self.assertTrue(dates.validate("2017-04-04T10:30:29-04:00"))
18
- self.assertTrue(dates.validate(datetime.utcnow().isoformat()))
18
+ self.assertTrue(dates.validate(datetime.now(timezone.utc).isoformat()))
19
19
  self.assertFalse(dates.validate("01-02-2003"))
20
20
  self.assertFalse(dates.validate("Thursday 21 March 2017"))
21
21
 
@@ -46,13 +46,13 @@ class DatesTest(unittest.TestCase):
46
46
  self.assertEqual(dates.clean("2017-5-2T10:00:00"), "2017-05-02T10:00:00")
47
47
 
48
48
  def test_convert_datetime(self):
49
- dt = datetime.utcnow()
49
+ dt = datetime.now(timezone.utc)
50
50
  iso, _ = dt.isoformat().split(".", 1)
51
51
  self.assertEqual(dates.clean(dt), iso)
52
52
  self.assertTrue(dates.validate(iso))
53
53
 
54
- dt = datetime.utcnow().date()
55
- iso = dt.isoformat()
54
+ dt = datetime.now(timezone.utc)
55
+ iso = dt.isoformat()[:19]
56
56
  self.assertEqual(dates.clean(dt), iso)
57
57
 
58
58
  def test_parse_date(self):
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"))