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 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
+
@@ -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