Skip to content

Commit 894a069

Browse files
authored
Merge pull request #408 from tabbols95/validators_inn
Validator russian individual tax number
2 parents da45d42 + ad2e2c5 commit 894a069

File tree

11 files changed

+146
-14
lines changed

11 files changed

+146
-14
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ cython_debug/
161161
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162162
# and can be added to the global gitignore or merged into this file. For a more nuclear
163163
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
164-
#.idea/
164+
.idea/
165165

166166
# VSCode
167167
.vscode/

package/export/__main__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ def _gen_rst_docs(source: Path, refs_path: Path, only_web: bool = False, only_ma
6666
with open(source / "docs/index.rst", "wt") as idx_f:
6767
idx_f.write(
6868
convert_file(source_file=source / "docs/index.md", format="md", to="rst").replace(
69-
"\r\n", "\n" # remove carriage return in windows
69+
"\r\n",
70+
"\n", # remove carriage return in windows
7071
)
7172
+ "\n\n.. toctree::"
7273
+ "\n :hidden:"

src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Validators."""

src/validators/domain.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ def domain(
8080
return False
8181

8282
try:
83-
8483
service_record = r"_" if rfc_2782 else ""
8584
trailing_dot = r"\.?$" if rfc_1034 else r"$"
8685

src/validators/i18n/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .fi import fi_business_id, fi_ssn
66
from .fr import fr_department, fr_ssn
77
from .ind import ind_aadhar, ind_pan
8+
from .ru import ru_inn
89

910
__all__ = (
1011
"fi_business_id",
@@ -17,4 +18,5 @@
1718
"fr_ssn",
1819
"ind_aadhar",
1920
"ind_pan",
21+
"ru_inn",
2022
)

src/validators/i18n/fi.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ def _ssn_pattern(ssn_check_marks: str):
2424
(\d{{2}}))
2525
[ABCDEFYXWVU+-]
2626
(?P<serial>(\d{{3}}))
27-
(?P<checksum>[{check_marks}])$""".format(
28-
check_marks=ssn_check_marks
29-
),
27+
(?P<checksum>[{check_marks}])$""".format(check_marks=ssn_check_marks),
3028
re.VERBOSE,
3129
)
3230

src/validators/i18n/ru.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Russia INN."""
2+
3+
from validators.utils import validator
4+
5+
6+
@validator
7+
def ru_inn(value: str):
8+
"""Validate a Russian INN (Taxpayer Identification Number).
9+
10+
The INN can be either 10 digits (for companies) or 12 digits (for individuals).
11+
The function checks both the length and the control digits according to Russian tax rules.
12+
13+
Examples:
14+
>>> ru_inn('500100732259') # Valid 12-digit INN
15+
True
16+
>>> ru_inn('7830002293') # Valid 10-digit INN
17+
True
18+
>>> ru_inn('1234567890') # Invalid INN
19+
ValidationFailure(func=ru_inn, args={'value': '1234567890'})
20+
21+
Args:
22+
value: Russian INN string to validate. Can contain only digits.
23+
24+
Returns:
25+
(Literal[True]): If `value` is a valid Russian INN.
26+
(ValidationError): If `value` is an invalid Russian INN.
27+
28+
Note:
29+
The validation follows the official algorithm:
30+
- For 10-digit INN: checks 10th control digit
31+
- For 12-digit INN: checks both 11th and 12th control digits
32+
"""
33+
if not value:
34+
return False
35+
36+
try:
37+
digits = list(map(int, value))
38+
# company
39+
if len(digits) == 10:
40+
weight_coefs = [2, 4, 10, 3, 5, 9, 4, 6, 8, 0]
41+
control_number = sum([d * w for d, w in zip(digits, weight_coefs)]) % 11
42+
return (
43+
(control_number % 10) == digits[-1]
44+
if control_number > 9
45+
else control_number == digits[-1]
46+
)
47+
# person
48+
elif len(digits) == 12:
49+
weight_coefs1 = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8, 0, 0]
50+
control_number1 = sum([d * w for d, w in zip(digits, weight_coefs1)]) % 11
51+
weight_coefs2 = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8, 0]
52+
control_number2 = sum([d * w for d, w in zip(digits, weight_coefs2)]) % 11
53+
print(control_number1, control_number2, value)
54+
return (
55+
(control_number1 % 10) == digits[-2]
56+
if control_number1 > 9
57+
else control_number1 == digits[-2] and (control_number2 % 10) == digits[-1]
58+
if control_number2 > 9
59+
else control_number2 == digits[-1]
60+
)
61+
else:
62+
return False
63+
except ValueError:
64+
return False

src/validators/uri.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,20 @@ def uri(value: str, /):
4747
# url
4848
if any(
4949
# fmt: off
50-
value.startswith(item) for item in {
51-
"ftp", "ftps", "git", "http", "https",
52-
"irc", "rtmp", "rtmps", "rtsp", "sftp",
53-
"ssh", "telnet",
50+
value.startswith(item)
51+
for item in {
52+
"ftp",
53+
"ftps",
54+
"git",
55+
"http",
56+
"https",
57+
"irc",
58+
"rtmp",
59+
"rtmps",
60+
"rtsp",
61+
"sftp",
62+
"ssh",
63+
"telnet",
5464
}
5565
# fmt: on
5666
):

src/validators/url.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,18 @@ def _validate_scheme(value: str):
4646
value
4747
# fmt: off
4848
in {
49-
"ftp", "ftps", "git", "http", "https",
50-
"irc", "rtmp", "rtmps", "rtsp", "sftp",
51-
"ssh", "telnet",
49+
"ftp",
50+
"ftps",
51+
"git",
52+
"http",
53+
"https",
54+
"irc",
55+
"rtmp",
56+
"rtmps",
57+
"rtsp",
58+
"sftp",
59+
"ssh",
60+
"telnet",
5261
}
5362
# fmt: on
5463
if value

tests/i18n/test_ru.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Test i18n/inn."""
2+
3+
# external
4+
import pytest
5+
6+
# local
7+
from validators import ValidationError
8+
from validators.i18n.ru import ru_inn
9+
10+
11+
@pytest.mark.parametrize(
12+
("value",),
13+
[
14+
("2222058686",),
15+
("7709439560",),
16+
("5003052454",),
17+
("7730257499",),
18+
("3664016814",),
19+
("026504247480",),
20+
("780103209220",),
21+
("7707012148",),
22+
("140700989885",),
23+
("774334078053",),
24+
],
25+
)
26+
def test_returns_true_on_valid_ru_inn(value: str):
27+
"""Test returns true on valid russian individual tax number."""
28+
assert ru_inn(value)
29+
30+
31+
@pytest.mark.parametrize(
32+
("value",),
33+
[
34+
("2222058687",),
35+
("7709439561",),
36+
("5003052453",),
37+
("7730257490",),
38+
("3664016815",),
39+
("026504247481",),
40+
("780103209222",),
41+
("7707012149",),
42+
("140700989886",),
43+
("774334078054",),
44+
],
45+
)
46+
def test_returns_false_on_valid_ru_inn(value: str):
47+
"""Test returns true on valid russian individual tax number."""
48+
assert isinstance(ru_inn(value), ValidationError)

tests/test_url.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def test_returns_true_on_valid_private_url(value: str, private: Optional[bool]):
156156
":// should fail",
157157
"http://foo.bar/foo(bar)baz quux",
158158
"http://-error-.invalid/",
159-
"http://www.\uFFFD.ch",
159+
"http://www.\ufffd.ch",
160160
"http://-a.b.co",
161161
"http://a.b-.co",
162162
"http://1.1.1.1.1",

0 commit comments

Comments
 (0)