human-input-validator 0.1.0__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.
- human_input_validator/__init__.py +25 -0
- human_input_validator/py.typed +0 -0
- human_input_validator/validators.py +134 -0
- human_input_validator-0.1.0.dist-info/METADATA +62 -0
- human_input_validator-0.1.0.dist-info/RECORD +7 -0
- human_input_validator-0.1.0.dist-info/WHEEL +5 -0
- human_input_validator-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Validation and normalization for common human-entered values."""
|
|
2
|
+
|
|
3
|
+
from .validators import (
|
|
4
|
+
ValidationError,
|
|
5
|
+
email,
|
|
6
|
+
country,
|
|
7
|
+
username,
|
|
8
|
+
phonenumber,
|
|
9
|
+
creditcard,
|
|
10
|
+
name,
|
|
11
|
+
lastname,
|
|
12
|
+
is_valid_url
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ValidationError",
|
|
17
|
+
"email",
|
|
18
|
+
"country",
|
|
19
|
+
"username",
|
|
20
|
+
"phonenumber",
|
|
21
|
+
"creditcard",
|
|
22
|
+
"name",
|
|
23
|
+
"lastname",
|
|
24
|
+
"is_valid_url"
|
|
25
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Pure validators for user-entered values.
|
|
2
|
+
|
|
3
|
+
Each function returns a normalized value or raises :class:`ValidationError`.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
import gettext
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import pycountry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ValidationError(ValueError):
|
|
17
|
+
"""Raised when a human-entered value cannot be validated."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_EMAIL_PATTERN = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
|
|
21
|
+
_USERNAME_PATTERN = re.compile(r"^[a-zA-Z0-9]+$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@functools.lru_cache(maxsize=1)
|
|
25
|
+
def _localized_country_names() -> dict[str, str]:
|
|
26
|
+
"""Map lower-cased, translated country names (e.g. 'deutschland') to alpha-2 codes."""
|
|
27
|
+
index: dict[str, str] = {}
|
|
28
|
+
for locale in os.listdir(pycountry.LOCALES_DIR):
|
|
29
|
+
try:
|
|
30
|
+
translation = gettext.translation("iso3166-1", pycountry.LOCALES_DIR, languages=[locale])
|
|
31
|
+
except FileNotFoundError:
|
|
32
|
+
continue
|
|
33
|
+
for entry in pycountry.countries:
|
|
34
|
+
for candidate in filter(None, (entry.name, getattr(entry, "official_name", None))):
|
|
35
|
+
translated = translation.gettext(candidate).strip().lower()
|
|
36
|
+
index.setdefault(translated, entry.alpha_2)
|
|
37
|
+
return index
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def email(value: str) -> str:
|
|
41
|
+
"""Return a trimmed, lower-case email address."""
|
|
42
|
+
normalized = value.strip().lower()
|
|
43
|
+
if not _EMAIL_PATTERN.fullmatch(normalized):
|
|
44
|
+
raise ValidationError("Enter a valid email address.")
|
|
45
|
+
return normalized
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def country(value: str) -> str:
|
|
49
|
+
"""Return the official name of an ISO 3166-1 country, given its name or code in any supported language."""
|
|
50
|
+
normalized = value.strip()
|
|
51
|
+
try:
|
|
52
|
+
result = pycountry.countries.lookup(normalized)
|
|
53
|
+
return result.name
|
|
54
|
+
except LookupError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
alpha_2 = _localized_country_names().get(normalized.lower())
|
|
58
|
+
if alpha_2 is None:
|
|
59
|
+
raise ValidationError("Please enter a valid country")
|
|
60
|
+
result = pycountry.countries.get(alpha_2=alpha_2)
|
|
61
|
+
if result is None:
|
|
62
|
+
raise ValidationError("Please enter a valid country")
|
|
63
|
+
return result.name
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def username(value: str, max_length: int = 12) -> str:
|
|
67
|
+
"""Checks a username length and allowed characters"""
|
|
68
|
+
normalized = value.strip()
|
|
69
|
+
if not _USERNAME_PATTERN.fullmatch(normalized):
|
|
70
|
+
raise ValidationError("Enter a valid username.")
|
|
71
|
+
if len(value) > max_length:
|
|
72
|
+
raise ValidationError(f"Username can be max {max_length} letters long")
|
|
73
|
+
|
|
74
|
+
return normalized.lower()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def phonenumber(value: str, pattern: str | list[str]) -> str:
|
|
78
|
+
"""Checks a phone number against one or more patterns, where '*' or '#' matches any digit."""
|
|
79
|
+
normalized = value.strip()
|
|
80
|
+
patterns = [pattern] if isinstance(pattern, str) else pattern
|
|
81
|
+
if not any(_matches_phone_pattern(normalized, candidate) for candidate in patterns):
|
|
82
|
+
raise ValidationError("Enter a valid phone number.")
|
|
83
|
+
return normalized
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _matches_phone_pattern(value: str, pattern: str) -> bool:
|
|
87
|
+
if len(value) != len(pattern):
|
|
88
|
+
return False
|
|
89
|
+
return all(v.isdigit() if p in "*#" else v == p for v, p in zip(value, pattern))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def creditcard(value: str) -> str:
|
|
93
|
+
"""Checks a creditcard number to be almost valid."""
|
|
94
|
+
normalized = re.sub(r"[\s-]", "", value.strip())
|
|
95
|
+
if not normalized.isdigit():
|
|
96
|
+
raise ValidationError("Enter a valid creditcard number.")
|
|
97
|
+
valid = False
|
|
98
|
+
if len(normalized) >= 13 and len(normalized) <= 19:
|
|
99
|
+
digits = [int(d) for d in reversed(normalized)]
|
|
100
|
+
for i in range(1, len(digits), 2):
|
|
101
|
+
digits[i] = digits[i] * 2 - 9 if digits[i] * 2 > 9 else digits[i] * 2
|
|
102
|
+
valid = sum(digits) % 10 == 0
|
|
103
|
+
|
|
104
|
+
if not valid:
|
|
105
|
+
raise ValidationError("Enter a valid creditcard number.")
|
|
106
|
+
return normalized
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def name(value: str) -> str:
|
|
110
|
+
"""Return a trimmed name with each part capitalized."""
|
|
111
|
+
normalized = value.strip()
|
|
112
|
+
if not normalized.replace(" ", "").isalpha():
|
|
113
|
+
raise ValidationError("Enter a valid name.")
|
|
114
|
+
return " ".join(part.capitalize() for part in normalized.split())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def lastname(value: str) -> str:
|
|
118
|
+
"""Return a trimmed last name with each part capitalized."""
|
|
119
|
+
normalized = value.strip()
|
|
120
|
+
if not normalized.replace(" ", "").isalpha():
|
|
121
|
+
raise ValidationError("Enter a valid last name.")
|
|
122
|
+
return " ".join(part.capitalize() for part in normalized.split())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def is_valid_url(value: str) -> str:
|
|
126
|
+
"""Checks if the given value is a valid HTTP(S) URL."""
|
|
127
|
+
|
|
128
|
+
normalized = value.strip()
|
|
129
|
+
parsed = urllib.parse.urlparse(normalized)
|
|
130
|
+
|
|
131
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
132
|
+
raise ValidationError("Enter a valid URL.")
|
|
133
|
+
|
|
134
|
+
return normalized
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: human-input-validator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Small validators for common human-entered data.
|
|
5
|
+
Author: Snorlena
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pycountry>=26.2
|
|
10
|
+
|
|
11
|
+
# Human-input-validator
|
|
12
|
+
|
|
13
|
+
`Human-input-validator` validates and normalizes common data entered by people. It
|
|
14
|
+
provides validators for email addresses, countries, usernames, phone
|
|
15
|
+
numbers, names, and credit card numbers.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from human_input_validator import (
|
|
19
|
+
email,
|
|
20
|
+
country,
|
|
21
|
+
username,
|
|
22
|
+
phonenumber,
|
|
23
|
+
creditcard,
|
|
24
|
+
name,
|
|
25
|
+
lastname,
|
|
26
|
+
is_valid_url
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
customer_email = email("Ada.Lovelace@Example.COM")
|
|
30
|
+
customer_country = country("SWEDEN ")
|
|
31
|
+
customer_username = username("JohnDoe ")
|
|
32
|
+
customer_phone = phonenumber("08-0000000", "**-*******")
|
|
33
|
+
customer_card = creditcard("4111 1111 1111 1111")
|
|
34
|
+
customer_name = name("ada lovelace")
|
|
35
|
+
customer_lastname = lastname("lovelace")
|
|
36
|
+
customer_url = is_valid_url("https://example.com ")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Each function returns a normalized value or raises `ValidationError` for
|
|
40
|
+
invalid input.
|
|
41
|
+
|
|
42
|
+
- `email(value)` — returns a trimmed, lower-cased email address.
|
|
43
|
+
- `country(value)` — returns the official ISO 3166-1 country name, given a
|
|
44
|
+
name or code.
|
|
45
|
+
- `username(value, max_length=12)` — returns a lower-cased username, checking
|
|
46
|
+
length and allowed characters.
|
|
47
|
+
- `phonenumber(value, pattern)` — checks a phone number against one or more
|
|
48
|
+
masks, where `*` or `#` matches any digit and other characters must match
|
|
49
|
+
literally. `pattern` accepts a single mask or a list of masks.
|
|
50
|
+
- `creditcard(value)` — checks a credit card number's length and Luhn
|
|
51
|
+
checksum, ignoring spaces and dashes.
|
|
52
|
+
- `name(value)` — returns a trimmed, capitalized name.
|
|
53
|
+
- `lastname(value)` — returns a trimmed, capitalized last name.
|
|
54
|
+
- `is_valid_url` — checks if an http/https url has correct syntax and returns a trimmed one
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
Run the test suite without installing the package:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
62
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
human_input_validator/__init__.py,sha256=HLgZ38DP9ZgUw_h415Z20tgMjcNlRg-uiNvq_QvO7OU,396
|
|
2
|
+
human_input_validator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
human_input_validator/validators.py,sha256=ywywVm0RSf64etHoXw-fJuNzmD1NiVNAx7KS-2Zp92k,4668
|
|
4
|
+
human_input_validator-0.1.0.dist-info/METADATA,sha256=nV-GkdFuQY7oMw6QJstUuDEyypRu4GynFL7i_sio5tM,2040
|
|
5
|
+
human_input_validator-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
human_input_validator-0.1.0.dist-info/top_level.txt,sha256=LflQgEAa6I_lUWbGb0SG87GXH2Rb-_r6kVmgHkFa87M,22
|
|
7
|
+
human_input_validator-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
human_input_validator
|