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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ human_input_validator