valid8r 0.2.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.
Potentially problematic release.
This version of valid8r might be problematic. Click here for more details.
- valid8r-0.2.0/PKG-INFO +168 -0
- valid8r-0.2.0/README.md +143 -0
- valid8r-0.2.0/pyproject.toml +190 -0
- valid8r-0.2.0/valid8r/__init__.py +27 -0
- valid8r-0.2.0/valid8r/core/__init__.py +28 -0
- valid8r-0.2.0/valid8r/core/combinators.py +89 -0
- valid8r-0.2.0/valid8r/core/maybe.py +162 -0
- valid8r-0.2.0/valid8r/core/parsers.py +1354 -0
- valid8r-0.2.0/valid8r/core/validators.py +200 -0
- valid8r-0.2.0/valid8r/prompt/__init__.py +8 -0
- valid8r-0.2.0/valid8r/prompt/basic.py +190 -0
- valid8r-0.2.0/valid8r/py.typed +0 -0
- valid8r-0.2.0/valid8r/testing/__init__.py +32 -0
- valid8r-0.2.0/valid8r/testing/assertions.py +67 -0
- valid8r-0.2.0/valid8r/testing/generators.py +283 -0
- valid8r-0.2.0/valid8r/testing/mock_input.py +84 -0
valid8r-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: valid8r
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Clean, flexible input validation for Python applications
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: validation,input,cli,maybe-monad
|
|
7
|
+
Author: Mike Lane
|
|
8
|
+
Author-email: mikelane@gmail.com
|
|
9
|
+
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Dist: email-validator (>=2.3.0,<3.0.0)
|
|
19
|
+
Requires-Dist: pydantic (>=2.0)
|
|
20
|
+
Requires-Dist: pydantic-core (>=2.27.0,<3.0.0)
|
|
21
|
+
Requires-Dist: uuid-utils (>=0.11.0,<0.12.0)
|
|
22
|
+
Project-URL: Repository, https://github.com/mikelane/valid8r
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Valid8r
|
|
26
|
+
|
|
27
|
+
A clean, flexible input validation library for Python applications.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **Clean Type Parsing**: Parse strings to various Python types with robust error handling
|
|
32
|
+
- **Flexible Validation**: Chain validators and create custom validation rules
|
|
33
|
+
- **Monadic Error Handling**: Use Maybe monad for clean error propagation
|
|
34
|
+
- **Input Prompting**: Prompt users for input with built-in validation
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install valid8r
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from valid8r import (
|
|
46
|
+
parsers,
|
|
47
|
+
prompt,
|
|
48
|
+
validators,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Simple validation
|
|
52
|
+
age = prompt.ask(
|
|
53
|
+
"Enter your age: ",
|
|
54
|
+
parser=parsers.parse_int,
|
|
55
|
+
validator=validators.minimum(0) & validators.maximum(120)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
print(f"Your age is {age}")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### IP parsing helpers
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from valid8r.core.maybe import Success, Failure
|
|
65
|
+
from valid8r.core import parsers
|
|
66
|
+
|
|
67
|
+
# IPv4 / IPv6 / generic IP
|
|
68
|
+
for text in ["192.168.0.1", "::1", " 10.0.0.1 "]:
|
|
69
|
+
match parsers.parse_ip(text):
|
|
70
|
+
case Success(addr):
|
|
71
|
+
print("Parsed:", addr)
|
|
72
|
+
case Failure(err):
|
|
73
|
+
print("Error:", err)
|
|
74
|
+
|
|
75
|
+
# CIDR (strict by default)
|
|
76
|
+
match parsers.parse_cidr("10.0.0.0/8"):
|
|
77
|
+
case Success(net):
|
|
78
|
+
print("Network:", net) # 10.0.0.0/8
|
|
79
|
+
case Failure(err):
|
|
80
|
+
print("Error:", err)
|
|
81
|
+
|
|
82
|
+
# Non-strict masks host bits
|
|
83
|
+
match parsers.parse_cidr("10.0.0.1/24", strict=False):
|
|
84
|
+
case Success(net):
|
|
85
|
+
assert str(net) == "10.0.0.0/24"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### URL and Email helpers
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from valid8r.core.maybe import Success, Failure
|
|
92
|
+
from valid8r.core import parsers
|
|
93
|
+
|
|
94
|
+
# URL parsing
|
|
95
|
+
match parsers.parse_url("https://alice:pw@example.com:8443/x?q=1#top"):
|
|
96
|
+
case Success(u):
|
|
97
|
+
print(u.scheme, u.username, u.password, u.host, u.port)
|
|
98
|
+
case Failure(err):
|
|
99
|
+
print("Error:", err)
|
|
100
|
+
|
|
101
|
+
# Email parsing
|
|
102
|
+
match parsers.parse_email("First.Last+tag@Example.COM"):
|
|
103
|
+
case Success(e):
|
|
104
|
+
print(e.local, e.domain) # First.Last+tag example.com
|
|
105
|
+
case Failure(err):
|
|
106
|
+
print("Error:", err)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Testing Support
|
|
110
|
+
|
|
111
|
+
Valid8r includes testing utilities to help you verify your validation logic:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from valid8r import (
|
|
115
|
+
Maybe,
|
|
116
|
+
validators,
|
|
117
|
+
parsers,
|
|
118
|
+
prompt,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
from valid8r.testing import (
|
|
122
|
+
MockInputContext,
|
|
123
|
+
assert_maybe_success,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def validate_age(age: int) -> Maybe[int]:
|
|
127
|
+
return validators.minimum(0) & validators.maximum(120)(age)
|
|
128
|
+
|
|
129
|
+
# Test prompts with mock input
|
|
130
|
+
with MockInputContext(["yes"]):
|
|
131
|
+
result = prompt.ask("Continue? ", parser=parsers.parse_bool)
|
|
132
|
+
assert result.is_success()
|
|
133
|
+
assert result.value_or(False) == True
|
|
134
|
+
|
|
135
|
+
# Test validation functions
|
|
136
|
+
result = validate_age(42)
|
|
137
|
+
assert assert_maybe_success(result, 42)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
For more information, see the [Testing with Valid8r](docs/user_guide/testing.rst) guide.
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
This project uses Poetry for dependency management and Tox for testing.
|
|
145
|
+
|
|
146
|
+
### Setup
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Install Poetry
|
|
150
|
+
curl -sSL https://install.python-poetry.org | python3 -
|
|
151
|
+
|
|
152
|
+
# Install dependencies
|
|
153
|
+
poetry install
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Running Tests
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# Run all tests
|
|
160
|
+
poetry run tox
|
|
161
|
+
|
|
162
|
+
# Run BDD tests
|
|
163
|
+
poetry run tox -e bdd
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
MIT
|
|
168
|
+
|
valid8r-0.2.0/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Valid8r
|
|
2
|
+
|
|
3
|
+
A clean, flexible input validation library for Python applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Clean Type Parsing**: Parse strings to various Python types with robust error handling
|
|
8
|
+
- **Flexible Validation**: Chain validators and create custom validation rules
|
|
9
|
+
- **Monadic Error Handling**: Use Maybe monad for clean error propagation
|
|
10
|
+
- **Input Prompting**: Prompt users for input with built-in validation
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install valid8r
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from valid8r import (
|
|
22
|
+
parsers,
|
|
23
|
+
prompt,
|
|
24
|
+
validators,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Simple validation
|
|
28
|
+
age = prompt.ask(
|
|
29
|
+
"Enter your age: ",
|
|
30
|
+
parser=parsers.parse_int,
|
|
31
|
+
validator=validators.minimum(0) & validators.maximum(120)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
print(f"Your age is {age}")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### IP parsing helpers
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from valid8r.core.maybe import Success, Failure
|
|
41
|
+
from valid8r.core import parsers
|
|
42
|
+
|
|
43
|
+
# IPv4 / IPv6 / generic IP
|
|
44
|
+
for text in ["192.168.0.1", "::1", " 10.0.0.1 "]:
|
|
45
|
+
match parsers.parse_ip(text):
|
|
46
|
+
case Success(addr):
|
|
47
|
+
print("Parsed:", addr)
|
|
48
|
+
case Failure(err):
|
|
49
|
+
print("Error:", err)
|
|
50
|
+
|
|
51
|
+
# CIDR (strict by default)
|
|
52
|
+
match parsers.parse_cidr("10.0.0.0/8"):
|
|
53
|
+
case Success(net):
|
|
54
|
+
print("Network:", net) # 10.0.0.0/8
|
|
55
|
+
case Failure(err):
|
|
56
|
+
print("Error:", err)
|
|
57
|
+
|
|
58
|
+
# Non-strict masks host bits
|
|
59
|
+
match parsers.parse_cidr("10.0.0.1/24", strict=False):
|
|
60
|
+
case Success(net):
|
|
61
|
+
assert str(net) == "10.0.0.0/24"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### URL and Email helpers
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from valid8r.core.maybe import Success, Failure
|
|
68
|
+
from valid8r.core import parsers
|
|
69
|
+
|
|
70
|
+
# URL parsing
|
|
71
|
+
match parsers.parse_url("https://alice:pw@example.com:8443/x?q=1#top"):
|
|
72
|
+
case Success(u):
|
|
73
|
+
print(u.scheme, u.username, u.password, u.host, u.port)
|
|
74
|
+
case Failure(err):
|
|
75
|
+
print("Error:", err)
|
|
76
|
+
|
|
77
|
+
# Email parsing
|
|
78
|
+
match parsers.parse_email("First.Last+tag@Example.COM"):
|
|
79
|
+
case Success(e):
|
|
80
|
+
print(e.local, e.domain) # First.Last+tag example.com
|
|
81
|
+
case Failure(err):
|
|
82
|
+
print("Error:", err)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Testing Support
|
|
86
|
+
|
|
87
|
+
Valid8r includes testing utilities to help you verify your validation logic:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from valid8r import (
|
|
91
|
+
Maybe,
|
|
92
|
+
validators,
|
|
93
|
+
parsers,
|
|
94
|
+
prompt,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
from valid8r.testing import (
|
|
98
|
+
MockInputContext,
|
|
99
|
+
assert_maybe_success,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def validate_age(age: int) -> Maybe[int]:
|
|
103
|
+
return validators.minimum(0) & validators.maximum(120)(age)
|
|
104
|
+
|
|
105
|
+
# Test prompts with mock input
|
|
106
|
+
with MockInputContext(["yes"]):
|
|
107
|
+
result = prompt.ask("Continue? ", parser=parsers.parse_bool)
|
|
108
|
+
assert result.is_success()
|
|
109
|
+
assert result.value_or(False) == True
|
|
110
|
+
|
|
111
|
+
# Test validation functions
|
|
112
|
+
result = validate_age(42)
|
|
113
|
+
assert assert_maybe_success(result, 42)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
For more information, see the [Testing with Valid8r](docs/user_guide/testing.rst) guide.
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
This project uses Poetry for dependency management and Tox for testing.
|
|
121
|
+
|
|
122
|
+
### Setup
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Install Poetry
|
|
126
|
+
curl -sSL https://install.python-poetry.org | python3 -
|
|
127
|
+
|
|
128
|
+
# Install dependencies
|
|
129
|
+
poetry install
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Running Tests
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Run all tests
|
|
136
|
+
poetry run tox
|
|
137
|
+
|
|
138
|
+
# Run BDD tests
|
|
139
|
+
poetry run tox -e bdd
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "valid8r"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Clean, flexible input validation for Python applications"
|
|
5
|
+
authors = ["Mike Lane <mikelane@gmail.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
repository = "https://github.com/mikelane/valid8r"
|
|
9
|
+
keywords = ["validation", "input", "cli", "maybe-monad"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
]
|
|
18
|
+
packages = [{include = "valid8r"}]
|
|
19
|
+
include = [{ path = "valid8r/py.typed", format = "wheel" }]
|
|
20
|
+
|
|
21
|
+
[tool.poetry.dependencies]
|
|
22
|
+
python = ">=3.11,<4.0"
|
|
23
|
+
email-validator = "^2.3.0"
|
|
24
|
+
pydantic = ">=2.0"
|
|
25
|
+
pydantic-core = "^2.27.0"
|
|
26
|
+
# No external dependencies for core functionality
|
|
27
|
+
uuid-utils = "^0.11.0"
|
|
28
|
+
|
|
29
|
+
[tool.poetry.group.dev.dependencies]
|
|
30
|
+
fastapi = "^0.119.0"
|
|
31
|
+
httpx = "^0.28.1"
|
|
32
|
+
livereload = "^2.7.1"
|
|
33
|
+
mypy = "^1.17.1"
|
|
34
|
+
pre-commit = "^4.3.0"
|
|
35
|
+
uvicorn = "^0.37.0"
|
|
36
|
+
|
|
37
|
+
[tool.poetry.group.lint.dependencies]
|
|
38
|
+
isort = "^6.0.1"
|
|
39
|
+
ruff = "^0.12.8"
|
|
40
|
+
|
|
41
|
+
[tool.poetry.group.docs.dependencies]
|
|
42
|
+
myst-parser = "^4.0.1"
|
|
43
|
+
sphinx = "^8.2.3"
|
|
44
|
+
sphinx-autoapi = "^3.6.0"
|
|
45
|
+
sphinx-autodoc-typehints = "^3.2.0"
|
|
46
|
+
sphinx-copybutton = "^0.5.2"
|
|
47
|
+
sphinx-rtd-theme = "^3.0.2"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
[tool.poetry.group.test.dependencies]
|
|
51
|
+
behave = "^1.3.0"
|
|
52
|
+
coverage = "^7.10.2"
|
|
53
|
+
pytest = "^8.4.1"
|
|
54
|
+
pytest-cov = "^6.2.1"
|
|
55
|
+
pytest-mock = "^3.14.1"
|
|
56
|
+
pytest-sugar = "^1.0.0"
|
|
57
|
+
tox = "^4.28.4"
|
|
58
|
+
|
|
59
|
+
[tool.poetry.scripts]
|
|
60
|
+
docs-build = "scripts.docs:build"
|
|
61
|
+
docs-serve = "scripts.docs:serve"
|
|
62
|
+
|
|
63
|
+
[build-system]
|
|
64
|
+
requires = ["poetry-core>=1.0.0"]
|
|
65
|
+
build-backend = "poetry.core.masonry.api"
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
testpaths = ["tests"]
|
|
69
|
+
python_files = ["it_*.py", "test_*.py"]
|
|
70
|
+
python_functions = ["it_*"]
|
|
71
|
+
python_classes = ["Describe[A-Z]*"]
|
|
72
|
+
addopts = ["-s", "-ra", "-q", "-vv"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
[tool.isort]
|
|
76
|
+
line_length = 120
|
|
77
|
+
profile = "black"
|
|
78
|
+
multi_line_output = 3
|
|
79
|
+
force_grid_wrap = 2
|
|
80
|
+
include_trailing_comma = true
|
|
81
|
+
use_parentheses = true
|
|
82
|
+
ensure_newline_before_comments = true
|
|
83
|
+
|
|
84
|
+
known_first_party = ["valid8r"]
|
|
85
|
+
|
|
86
|
+
[tool.ruff]
|
|
87
|
+
line-length = 120
|
|
88
|
+
|
|
89
|
+
src = ["valid8r", "tests"]
|
|
90
|
+
|
|
91
|
+
include = ["*.py"]
|
|
92
|
+
|
|
93
|
+
exclude = [".git", ".venv", "__pycache__", "build", "dist"]
|
|
94
|
+
|
|
95
|
+
target-version = "py311"
|
|
96
|
+
|
|
97
|
+
respect-gitignore = true
|
|
98
|
+
|
|
99
|
+
[tool.ruff.format]
|
|
100
|
+
quote-style = "single"
|
|
101
|
+
indent-style = "space"
|
|
102
|
+
skip-magic-trailing-comma = false
|
|
103
|
+
line-ending = "auto"
|
|
104
|
+
|
|
105
|
+
[tool.ruff.lint]
|
|
106
|
+
select = ["ALL"]
|
|
107
|
+
ignore = [
|
|
108
|
+
"COM812", # Ignore missing trailing commas (conflicts with ruff format)
|
|
109
|
+
"D203", # Don't require 1 blank line before class docstring
|
|
110
|
+
"D213", # Don't require multiline docstring to start at the second line
|
|
111
|
+
"EM101", # Allow raw strings as error messages
|
|
112
|
+
"ISC001", # Ignore implicitly concatenated strings (conflicts with ruff format)
|
|
113
|
+
"PLR0911", # I don't agree that there could be too many return statements.
|
|
114
|
+
"T201", # This module needs to print, so don't prevent it
|
|
115
|
+
"TRY003", # Don't be so strict about the length of exception messages or whatever this is
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
119
|
+
|
|
120
|
+
[tool.ruff.lint.per-file-ignores]
|
|
121
|
+
"test_*.py" = [
|
|
122
|
+
"D100", # Don't require module docstrings
|
|
123
|
+
"D101", # Don't require class docstrings
|
|
124
|
+
"D102", # Don't require method docstrings
|
|
125
|
+
"FBT001", # Allow tests to use boolean arguments
|
|
126
|
+
"PLR2004", # Don't prevent magic values in tests
|
|
127
|
+
"S101", # Don't prevent asserts
|
|
128
|
+
]
|
|
129
|
+
"tests/*" = [
|
|
130
|
+
"D100", # Don't require module docstrings in tests
|
|
131
|
+
"D101", # Don't require class docstrings in tests
|
|
132
|
+
"D102", # Don't require method docstrings in tests
|
|
133
|
+
"D103", # Don't require function docstrings in tests
|
|
134
|
+
"D104", # Don't require init docstrings in tests
|
|
135
|
+
"PGH003", # Allow tests to casually ignore mypy complaints about incorrect types
|
|
136
|
+
"PLC0415", # Allow imports inside test functions
|
|
137
|
+
"PLR0913", # Allow tests to have as many arguments as we want
|
|
138
|
+
]
|
|
139
|
+
"tests/bdd/steps/*" = [
|
|
140
|
+
"S101", # Allow asserts in behave tests
|
|
141
|
+
]
|
|
142
|
+
"tests/_utils/check_coverage.py" = ["T201"] # allow print function in the scripts
|
|
143
|
+
"scripts/*" = ["T201"] # allow print function in the scripts
|
|
144
|
+
"smoke_test.py" = ["S101"] # allow asserts in smoke test
|
|
145
|
+
"conftest.py" = ["D100"] # Don't require module docstrings in conftest
|
|
146
|
+
"__init__.py" = ["F401"] # allow unused imports in __init__.py
|
|
147
|
+
"valid8r/core/parsers.py" = [
|
|
148
|
+
"C901", # Allow complex functions in parsers
|
|
149
|
+
"D413", # Allow missing blank line after last docstring section
|
|
150
|
+
"FBT001", # Allow boolean positional arguments
|
|
151
|
+
"FBT002", # Allow boolean default arguments
|
|
152
|
+
"PLR2004", # Allow magic values for network constants
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
[tool.ruff.lint.isort]
|
|
156
|
+
required-imports = ["from __future__ import annotations"]
|
|
157
|
+
known-first-party = ["valid8r"]
|
|
158
|
+
|
|
159
|
+
[tool.ruff.lint.flake8-quotes]
|
|
160
|
+
inline-quotes = "single"
|
|
161
|
+
multiline-quotes = "double"
|
|
162
|
+
docstring-quotes = "double"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
[tool.mypy]
|
|
166
|
+
python_version = "3.11"
|
|
167
|
+
warn_return_any = true
|
|
168
|
+
warn_unused_configs = true
|
|
169
|
+
disallow_untyped_defs = true
|
|
170
|
+
disallow_incomplete_defs = true
|
|
171
|
+
|
|
172
|
+
[[tool.mypy.overrides]]
|
|
173
|
+
module = [
|
|
174
|
+
"pytest",
|
|
175
|
+
"pytest.*",
|
|
176
|
+
"pytest_mock",
|
|
177
|
+
"pytest_mock.*",
|
|
178
|
+
"behave",
|
|
179
|
+
"behave.*",
|
|
180
|
+
"sphinx",
|
|
181
|
+
"sphinx.*",
|
|
182
|
+
]
|
|
183
|
+
ignore_missing_imports = true
|
|
184
|
+
|
|
185
|
+
[[tool.mypy.overrides]]
|
|
186
|
+
module = [
|
|
187
|
+
"tests.*",
|
|
188
|
+
"docs.*",
|
|
189
|
+
]
|
|
190
|
+
ignore_errors = true
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# valid8r/__init__.py
|
|
2
|
+
"""Valid8r: A clean, flexible input validation library for Python."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
__version__ = '0.1.0'
|
|
7
|
+
|
|
8
|
+
# Public API re-exports for concise imports
|
|
9
|
+
# Modules
|
|
10
|
+
from . import prompt
|
|
11
|
+
from .core import (
|
|
12
|
+
combinators,
|
|
13
|
+
parsers,
|
|
14
|
+
validators,
|
|
15
|
+
)
|
|
16
|
+
from .core.maybe import Maybe
|
|
17
|
+
|
|
18
|
+
# Types
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
'Maybe',
|
|
22
|
+
'__version__',
|
|
23
|
+
'combinators',
|
|
24
|
+
'parsers',
|
|
25
|
+
'prompt',
|
|
26
|
+
'validators',
|
|
27
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# valid8r/core/__init__.py
|
|
2
|
+
"""Core validation components."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from valid8r.core.parsers import (
|
|
7
|
+
EmailAddress,
|
|
8
|
+
UrlParts,
|
|
9
|
+
parse_cidr,
|
|
10
|
+
parse_email,
|
|
11
|
+
parse_ip,
|
|
12
|
+
parse_ipv4,
|
|
13
|
+
parse_ipv6,
|
|
14
|
+
parse_url,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
'EmailAddress',
|
|
19
|
+
'UrlParts',
|
|
20
|
+
'parse_cidr',
|
|
21
|
+
'parse_email',
|
|
22
|
+
'parse_ip',
|
|
23
|
+
# existing exports may be defined elsewhere; explicitly expose IP helpers
|
|
24
|
+
'parse_ipv4',
|
|
25
|
+
'parse_ipv6',
|
|
26
|
+
# URL/Email helpers
|
|
27
|
+
'parse_url',
|
|
28
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Combinators for creating complex validation rules.
|
|
2
|
+
|
|
3
|
+
This module provides functions to combine validators using logical operations like AND, OR, and NOT.
|
|
4
|
+
These combinators allow for creation of complex validation chains.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import (
|
|
10
|
+
TYPE_CHECKING,
|
|
11
|
+
TypeVar,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from valid8r.core.maybe import (
|
|
15
|
+
Maybe,
|
|
16
|
+
Success,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
|
|
22
|
+
T = TypeVar('T')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def and_then(first: Callable[[T], Maybe[T]], second: Callable[[T], Maybe[T]]) -> Callable[[T], Maybe[T]]:
|
|
26
|
+
"""Combine two validators with logical AND (both must succeed).
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
first: The first validator function
|
|
30
|
+
second: The second validator function
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A new validator function that passes only if both validators pass
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def combined_validator(value: T) -> Maybe[T]:
|
|
38
|
+
result = first(value)
|
|
39
|
+
match result:
|
|
40
|
+
case Success(value):
|
|
41
|
+
return second(value)
|
|
42
|
+
case _:
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
return combined_validator
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def or_else(first: Callable[[T], Maybe[T]], second: Callable[[T], Maybe[T]]) -> Callable[[T], Maybe[T]]:
|
|
49
|
+
"""Combine two validators with logical OR (either can succeed).
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
first: The first validator function
|
|
53
|
+
second: The second validator function
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A new validator function that passes if either validator passes
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def combined_validator(value: T) -> Maybe[T]:
|
|
61
|
+
result = first(value)
|
|
62
|
+
match result:
|
|
63
|
+
case Success(value):
|
|
64
|
+
return result
|
|
65
|
+
case _:
|
|
66
|
+
return second(value)
|
|
67
|
+
|
|
68
|
+
return combined_validator
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def not_validator(validator: Callable[[T], Maybe[T]], error_message: str) -> Callable[[T], Maybe[T]]:
|
|
72
|
+
"""Negate a validator (success becomes failure and vice versa).
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
validator: The validator function to negate
|
|
76
|
+
error_message: Error message for when the negated validator fails
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A new validator function that passes if the original validator fails
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def negated_validator(value: T) -> Maybe[T]:
|
|
84
|
+
result = validator(value)
|
|
85
|
+
if result.is_failure():
|
|
86
|
+
return Maybe.success(value)
|
|
87
|
+
return Maybe.failure(error_message)
|
|
88
|
+
|
|
89
|
+
return negated_validator
|