human-input-validator 0.1.0__tar.gz

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,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,52 @@
1
+ # Human-input-validator
2
+
3
+ `Human-input-validator` validates and normalizes common data entered by people. It
4
+ provides validators for email addresses, countries, usernames, phone
5
+ numbers, names, and credit card numbers.
6
+
7
+ ```python
8
+ from human_input_validator import (
9
+ email,
10
+ country,
11
+ username,
12
+ phonenumber,
13
+ creditcard,
14
+ name,
15
+ lastname,
16
+ is_valid_url
17
+ )
18
+
19
+ customer_email = email("Ada.Lovelace@Example.COM")
20
+ customer_country = country("SWEDEN ")
21
+ customer_username = username("JohnDoe ")
22
+ customer_phone = phonenumber("08-0000000", "**-*******")
23
+ customer_card = creditcard("4111 1111 1111 1111")
24
+ customer_name = name("ada lovelace")
25
+ customer_lastname = lastname("lovelace")
26
+ customer_url = is_valid_url("https://example.com ")
27
+ ```
28
+
29
+ Each function returns a normalized value or raises `ValidationError` for
30
+ invalid input.
31
+
32
+ - `email(value)` — returns a trimmed, lower-cased email address.
33
+ - `country(value)` — returns the official ISO 3166-1 country name, given a
34
+ name or code.
35
+ - `username(value, max_length=12)` — returns a lower-cased username, checking
36
+ length and allowed characters.
37
+ - `phonenumber(value, pattern)` — checks a phone number against one or more
38
+ masks, where `*` or `#` matches any digit and other characters must match
39
+ literally. `pattern` accepts a single mask or a list of masks.
40
+ - `creditcard(value)` — checks a credit card number's length and Luhn
41
+ checksum, ignoring spaces and dashes.
42
+ - `name(value)` — returns a trimmed, capitalized name.
43
+ - `lastname(value)` — returns a trimmed, capitalized last name.
44
+ - `is_valid_url` — checks if an http/https url has correct syntax and returns a trimmed one
45
+
46
+ ## Development
47
+
48
+ Run the test suite without installing the package:
49
+
50
+ ```sh
51
+ PYTHONPATH=src python3 -m unittest discover -s tests -v
52
+ ```
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "human-input-validator"
7
+ version = "0.1.0"
8
+ description = "Small validators for common human-entered data."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = ["pycountry>=26.2"]
12
+ license = "MIT"
13
+ authors = [{name = "Snorlena"}]
14
+
15
+ [tool.black]
16
+ target-version = ["py310"]
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["src"]
20
+
21
+ [tool.setuptools.package-data]
22
+ human_input_validator = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]
@@ -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,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/human_input_validator/__init__.py
4
+ src/human_input_validator/py.typed
5
+ src/human_input_validator/validators.py
6
+ src/human_input_validator.egg-info/PKG-INFO
7
+ src/human_input_validator.egg-info/SOURCES.txt
8
+ src/human_input_validator.egg-info/dependency_links.txt
9
+ src/human_input_validator.egg-info/requires.txt
10
+ src/human_input_validator.egg-info/top_level.txt
11
+ tests/test_validators.py
@@ -0,0 +1,97 @@
1
+ """Tests for humaninput validators."""
2
+
3
+ # pylint: disable=missing-class-docstring,missing-function-docstring
4
+ import unittest
5
+
6
+ from human_input_validator import (
7
+ ValidationError,
8
+ email,
9
+ country,
10
+ username,
11
+ phonenumber,
12
+ creditcard,
13
+ name,
14
+ lastname,
15
+ is_valid_url,
16
+ )
17
+
18
+
19
+ class EmailTests(unittest.TestCase):
20
+ def test_normalizes_email(self) -> None:
21
+ self.assertEqual(
22
+ email(" Ada.Lovelace@Example.COM "), "ada.lovelace@example.com"
23
+ )
24
+
25
+ def test_rejects_invalid_email(self) -> None:
26
+ with self.assertRaises(ValidationError):
27
+ email("not-an-email")
28
+
29
+
30
+ class CountryTests(unittest.TestCase):
31
+ def test_normalizes_country(self) -> None:
32
+ self.assertEqual(country("SWEDEN "), "Sweden")
33
+
34
+ def test_rejects_country(self) -> None:
35
+ with self.assertRaises(ValidationError):
36
+ country("Kolbäck")
37
+
38
+
39
+ class UsernameTests(unittest.TestCase):
40
+ def test_normalizes_username(self) -> None:
41
+ self.assertEqual(username("JohnDoe "), "johndoe")
42
+
43
+ def test_rejects_username(self) -> None:
44
+ with self.assertRaises(ValidationError):
45
+ username("#my_name")
46
+
47
+
48
+ class PhonenumberTests(unittest.TestCase):
49
+ def test_normalizes_phonenumber(self) -> None:
50
+ self.assertEqual(
51
+ phonenumber("0760000000", ["*#-*#*#*#*", "##########", "**********"]),
52
+ "0760000000",
53
+ )
54
+
55
+ def test_rejects_phonenumber(self) -> None:
56
+ with self.assertRaises(ValidationError):
57
+ phonenumber("08-0000000", "***-*********")
58
+
59
+
60
+ class CreditcardTests(unittest.TestCase):
61
+ def test_normalizes_creditcard(self) -> None:
62
+ self.assertEqual(creditcard("0000-0000-0000-0000"), "0000000000000000")
63
+
64
+ def test_rejects_creditcard(self) -> None:
65
+ with self.assertRaises(ValidationError):
66
+ creditcard("0123-4567-8901-2345")
67
+
68
+
69
+ class NameTests(unittest.TestCase):
70
+ def test_normalizes_name(self) -> None:
71
+ self.assertEqual(name(" ada lovelace "), "Ada Lovelace")
72
+
73
+ def test_rejects_name(self) -> None:
74
+ with self.assertRaises(ValidationError):
75
+ name("Ada123")
76
+
77
+
78
+ class LastnameTests(unittest.TestCase):
79
+ def test_normalizes_lastname(self) -> None:
80
+ self.assertEqual(lastname(" lovelace "), "Lovelace")
81
+
82
+ def test_rejects_lastname(self) -> None:
83
+ with self.assertRaises(ValidationError):
84
+ lastname("Lovelace123")
85
+
86
+
87
+ class IsValidUrlTests(unittest.TestCase):
88
+ def test_normalizes_url(self) -> None:
89
+ self.assertEqual(is_valid_url(" https://example.com "), "https://example.com")
90
+
91
+ def test_rejects_invalid_url(self) -> None:
92
+ with self.assertRaises(ValidationError):
93
+ is_valid_url("not-a-url")
94
+
95
+
96
+ if __name__ == "__main__":
97
+ unittest.main()