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.
- human_input_validator-0.1.0/PKG-INFO +62 -0
- human_input_validator-0.1.0/README.md +52 -0
- human_input_validator-0.1.0/pyproject.toml +22 -0
- human_input_validator-0.1.0/setup.cfg +4 -0
- human_input_validator-0.1.0/src/human_input_validator/__init__.py +25 -0
- human_input_validator-0.1.0/src/human_input_validator/py.typed +0 -0
- human_input_validator-0.1.0/src/human_input_validator/validators.py +134 -0
- human_input_validator-0.1.0/src/human_input_validator.egg-info/PKG-INFO +62 -0
- human_input_validator-0.1.0/src/human_input_validator.egg-info/SOURCES.txt +11 -0
- human_input_validator-0.1.0/src/human_input_validator.egg-info/dependency_links.txt +1 -0
- human_input_validator-0.1.0/src/human_input_validator.egg-info/requires.txt +1 -0
- human_input_validator-0.1.0/src/human_input_validator.egg-info/top_level.txt +1 -0
- human_input_validator-0.1.0/tests/test_validators.py +97 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pycountry>=26.2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
human_input_validator
|
|
@@ -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()
|