valid8r 0.7.2__tar.gz → 0.7.4__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.7.4/PKG-INFO +305 -0
- valid8r-0.7.4/README.md +274 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/pyproject.toml +1 -1
- {valid8r-0.7.2 → valid8r-0.7.4}/uv.lock +1 -1
- valid8r-0.7.2/PKG-INFO +0 -306
- valid8r-0.7.2/README.md +0 -275
- {valid8r-0.7.2 → valid8r-0.7.4}/.coveragerc +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.cursorrules +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/CICD_TEST.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/CODEOWNERS +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/CONVENTIONAL_COMMITS.md +0 -0
- /valid8r-0.7.2/.github/README.md → /valid8r-0.7.4/.github/GITHUB_CONFIG.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/ISSUE_TEMPLATE/documentation.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/PYPI_TOKEN_SETUP_GUIDE.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/QUICK_REFERENCE.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/SETUP_CHECKLIST.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/WORKFLOWS.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/WORKFLOW_DIAGRAM.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/dependabot.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/labeler.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/pull_request_template.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/release.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/workflows/ci.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/workflows/labeler.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/workflows/publish-pypi.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/workflows/semantic-release.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/workflows/size-label.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/workflows/stale.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/workflows/version-and-release.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.github/workflows/welcome.yml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.gitignore +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.pre-commit-config.yaml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.python-version +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/.readthedocs.yaml +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/CICD_SETUP_SUMMARY.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/CLAUDE.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/CODE_OF_CONDUCT.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/CONTRIBUTING.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/LICENSE +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/QA_REPORT_WEB_PARSERS_V0.6.0.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/QA_VALIDATION_SUMMARY.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/ROADMAP.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/SECURITY.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/_static/css/custom.css +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/api/core.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/api/prompt.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/conf.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/development/changelog.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/development/contributing.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/development/testing.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/examples/basic_example.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/examples/chaining_validators.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/examples/custom_validators.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/examples/fastapi_integration.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/examples/interactive_prompts.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/index.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/migration-poetry-to-uv.md +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/user_guide/advanced_usage.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/user_guide/getting_started.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/user_guide/maybe_monad.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/user_guide/parsers.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/user_guide/prompting.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/user_guide/testing.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/docs/user_guide/validators.rst +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/scripts/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/scripts/docs.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/smoke_test.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/conftest.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/environment.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/features/clean_type_parsing.feature +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/features/collection_parsing.feature +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/features/interactive_prompts.feature +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/features/phone_parsing.feature +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/features/testing_utilities.feature +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/features/url_email_parsing.feature +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/features/validator_combinators.feature +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/features/validators.feature +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/features/web_parsers.feature +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/steps/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/steps/clean_type_parsing_steps.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/steps/collection_parsing_steps.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/steps/interactive_prompts_steps.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/steps/phone_parsing_steps.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/steps/testing_utilities_steps.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/steps/url_email_parsing_steps.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/steps/validator_combinators_steps.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/steps/validators_steps.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/bdd/steps/web_parsers_steps.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/integration/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/integration/test_validator.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/qa_security_web_parsers.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/conftest.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_combinators.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_decimal_parser.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_dict_parser.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_generators.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_ip_parsers.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_list_parser.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_maybe.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_parsers.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_phone_parsing.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_prompt.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_public_api.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_testing_utilities.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_url_email_parsers.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_uuid_parser.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_validators.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tests/unit/test_web_parsers.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/tox.ini +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/core/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/core/combinators.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/core/maybe.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/core/parsers.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/core/validators.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/prompt/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/prompt/basic.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/py.typed +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/testing/__init__.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/testing/assertions.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/testing/generators.py +0 -0
- {valid8r-0.7.2 → valid8r-0.7.4}/valid8r/testing/mock_input.py +0 -0
valid8r-0.7.4/PKG-INFO
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: valid8r
|
|
3
|
+
Version: 0.7.4
|
|
4
|
+
Summary: Clean, flexible input validation for Python applications
|
|
5
|
+
Project-URL: Homepage, https://valid8r.readthedocs.io/
|
|
6
|
+
Project-URL: Repository, https://github.com/mikelane/valid8r
|
|
7
|
+
Project-URL: Documentation, https://valid8r.readthedocs.io/
|
|
8
|
+
Project-URL: Issues, https://github.com/mikelane/valid8r/issues
|
|
9
|
+
Author-email: Mike Lane <mikelane@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cli,functional-programming,input,maybe-monad,parsing,validation
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: email-validator>=2.3.0
|
|
27
|
+
Requires-Dist: pydantic-core>=2.27.0
|
|
28
|
+
Requires-Dist: pydantic>=2.0
|
|
29
|
+
Requires-Dist: uuid-utils>=0.11.0
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Valid8r
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/valid8r/)
|
|
35
|
+
[](https://pypi.org/project/valid8r/)
|
|
36
|
+
[](https://github.com/mikelane/valid8r/blob/main/LICENSE)
|
|
37
|
+
[](https://github.com/mikelane/valid8r/actions)
|
|
38
|
+
[](https://codecov.io/gh/mikelane/valid8r)
|
|
39
|
+
[](https://valid8r.readthedocs.io/)
|
|
40
|
+
|
|
41
|
+
**Clean, composable input validation for Python using functional programming patterns.**
|
|
42
|
+
|
|
43
|
+
Valid8r makes input validation elegant and type-safe by using the Maybe monad for error handling. No more try-except blocks or boolean validation chains—just clean, composable parsers that tell you exactly what went wrong.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from valid8r import parsers, validators, prompt
|
|
47
|
+
|
|
48
|
+
# Parse and validate user input with rich error messages
|
|
49
|
+
age = prompt.ask(
|
|
50
|
+
"Enter your age: ",
|
|
51
|
+
parser=parsers.parse_int,
|
|
52
|
+
validator=validators.minimum(0) & validators.maximum(120)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
print(f"Your age is {age}")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Why Valid8r?
|
|
59
|
+
|
|
60
|
+
**Type-Safe Parsing**: Every parser returns `Maybe[T]` (Success or Failure), making error handling explicit and composable.
|
|
61
|
+
|
|
62
|
+
**Rich Structured Results**: Network parsers return dataclasses with parsed components—no more manual URL/email splitting.
|
|
63
|
+
|
|
64
|
+
**Chainable Validators**: Combine validators using `&` (and), `|` (or), and `~` (not) operators for complex validation logic.
|
|
65
|
+
|
|
66
|
+
**Zero Dependencies**: Core library uses only Python's standard library (with optional `uuid-utils` for faster UUID parsing).
|
|
67
|
+
|
|
68
|
+
**Interactive Prompts**: Built-in user input prompting with automatic retry and validation.
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
### Installation
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install valid8r
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Requirements**: Python 3.11 or higher
|
|
79
|
+
|
|
80
|
+
### Basic Parsing
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from valid8r import parsers
|
|
84
|
+
from valid8r.core.maybe import Success, Failure
|
|
85
|
+
|
|
86
|
+
# Parse integers with automatic error handling
|
|
87
|
+
match parsers.parse_int("42"):
|
|
88
|
+
case Success(value):
|
|
89
|
+
print(f"Parsed: {value}") # Parsed: 42
|
|
90
|
+
case Failure(error):
|
|
91
|
+
print(f"Error: {error}")
|
|
92
|
+
|
|
93
|
+
# Parse dates (ISO 8601 format)
|
|
94
|
+
result = parsers.parse_date("2025-01-15")
|
|
95
|
+
assert result.is_success()
|
|
96
|
+
|
|
97
|
+
# Parse UUIDs with version validation
|
|
98
|
+
result = parsers.parse_uuid("550e8400-e29b-41d4-a716-446655440000", version=4)
|
|
99
|
+
assert result.is_success()
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Validation with Combinators
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from valid8r import validators
|
|
106
|
+
|
|
107
|
+
# Combine validators using operators
|
|
108
|
+
age_validator = validators.minimum(0) & validators.maximum(120)
|
|
109
|
+
result = age_validator(42)
|
|
110
|
+
assert result.is_success()
|
|
111
|
+
|
|
112
|
+
# String validation
|
|
113
|
+
password_validator = (
|
|
114
|
+
validators.length(8, 128) &
|
|
115
|
+
validators.matches_regex(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]')
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Set validation
|
|
119
|
+
tags_validator = validators.subset_of({'python', 'rust', 'go', 'typescript'})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Structured Network Parsing
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from valid8r import parsers
|
|
126
|
+
|
|
127
|
+
# Parse URLs into structured components
|
|
128
|
+
match parsers.parse_url("https://user:pass@example.com:8443/path?query=1#fragment"):
|
|
129
|
+
case Success(url):
|
|
130
|
+
print(f"Scheme: {url.scheme}") # https
|
|
131
|
+
print(f"Host: {url.host}") # example.com
|
|
132
|
+
print(f"Port: {url.port}") # 8443
|
|
133
|
+
print(f"Path: {url.path}") # /path
|
|
134
|
+
print(f"Query: {url.query}") # {'query': '1'}
|
|
135
|
+
print(f"Fragment: {url.fragment}") # fragment
|
|
136
|
+
|
|
137
|
+
# Parse emails with normalized domains
|
|
138
|
+
match parsers.parse_email("User@Example.COM"):
|
|
139
|
+
case Success(email):
|
|
140
|
+
print(f"Local: {email.local}") # User
|
|
141
|
+
print(f"Domain: {email.domain}") # example.com (normalized)
|
|
142
|
+
|
|
143
|
+
# Parse phone numbers (NANP format)
|
|
144
|
+
match parsers.parse_phone("+1 (415) 555-2671"):
|
|
145
|
+
case Success(phone):
|
|
146
|
+
print(f"E.164: {phone.e164}") # +14155552671
|
|
147
|
+
print(f"National: {phone.national}") # (415) 555-2671
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Collection Parsing
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from valid8r import parsers
|
|
154
|
+
|
|
155
|
+
# Parse lists with element validation
|
|
156
|
+
result = parsers.parse_list("1,2,3,4,5", element_parser=parsers.parse_int)
|
|
157
|
+
assert result.value_or([]) == [1, 2, 3, 4, 5]
|
|
158
|
+
|
|
159
|
+
# Parse dictionaries with key/value parsers
|
|
160
|
+
result = parsers.parse_dict(
|
|
161
|
+
"name=Alice,age=30",
|
|
162
|
+
key_parser=lambda x: Success(x),
|
|
163
|
+
value_parser=lambda x: parsers.parse_int(x) if x.isdigit() else Success(x)
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Interactive Prompting
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from valid8r import prompt, parsers, validators
|
|
171
|
+
|
|
172
|
+
# Prompt with validation and automatic retry
|
|
173
|
+
email = prompt.ask(
|
|
174
|
+
"Email address: ",
|
|
175
|
+
parser=parsers.parse_email,
|
|
176
|
+
retry=2 # Retry twice on invalid input
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Combine parsing and validation
|
|
180
|
+
port = prompt.ask(
|
|
181
|
+
"Server port: ",
|
|
182
|
+
parser=parsers.parse_int,
|
|
183
|
+
validator=validators.between(1024, 65535),
|
|
184
|
+
retry=3
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Features
|
|
189
|
+
|
|
190
|
+
### Parsers
|
|
191
|
+
|
|
192
|
+
**Basic Types**:
|
|
193
|
+
- `parse_int`, `parse_float`, `parse_bool`, `parse_decimal`, `parse_complex`
|
|
194
|
+
- `parse_date` (ISO 8601), `parse_uuid` (with version validation)
|
|
195
|
+
|
|
196
|
+
**Collections**:
|
|
197
|
+
- `parse_list`, `parse_dict`, `parse_set` (with element parsers)
|
|
198
|
+
|
|
199
|
+
**Network & Communication**:
|
|
200
|
+
- `parse_ipv4`, `parse_ipv6`, `parse_ip`, `parse_cidr`
|
|
201
|
+
- `parse_url` → `UrlParts` (structured URL components)
|
|
202
|
+
- `parse_email` → `EmailAddress` (normalized domain)
|
|
203
|
+
- `parse_phone` → `PhoneNumber` (NANP validation with E.164 formatting)
|
|
204
|
+
|
|
205
|
+
**Advanced**:
|
|
206
|
+
- `parse_enum` (type-safe enum parsing)
|
|
207
|
+
- `create_parser`, `make_parser`, `validated_parser` (custom parser factories)
|
|
208
|
+
|
|
209
|
+
### Validators
|
|
210
|
+
|
|
211
|
+
**Numeric**: `minimum`, `maximum`, `between`
|
|
212
|
+
|
|
213
|
+
**String**: `non_empty_string`, `matches_regex`, `length`
|
|
214
|
+
|
|
215
|
+
**Collection**: `in_set`, `unique_items`, `subset_of`, `superset_of`, `is_sorted`
|
|
216
|
+
|
|
217
|
+
**Custom**: `predicate` (create validators from any function)
|
|
218
|
+
|
|
219
|
+
**Combinators**: Combine validators using `&` (and), `|` (or), `~` (not)
|
|
220
|
+
|
|
221
|
+
### Testing Utilities
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from valid8r.testing import (
|
|
225
|
+
assert_maybe_success,
|
|
226
|
+
assert_maybe_failure,
|
|
227
|
+
MockInputContext,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Test validation logic
|
|
231
|
+
result = validators.minimum(0)(42)
|
|
232
|
+
assert assert_maybe_success(result, 42)
|
|
233
|
+
|
|
234
|
+
result = validators.minimum(0)(-5)
|
|
235
|
+
assert assert_maybe_failure(result, "at least 0")
|
|
236
|
+
|
|
237
|
+
# Mock user input for testing prompts
|
|
238
|
+
with MockInputContext(["invalid", "valid@example.com"]):
|
|
239
|
+
result = prompt.ask("Email: ", parser=parsers.parse_email, retry=1)
|
|
240
|
+
assert result.is_success()
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Documentation
|
|
244
|
+
|
|
245
|
+
**Full documentation**: [valid8r.readthedocs.io](https://valid8r.readthedocs.io/)
|
|
246
|
+
|
|
247
|
+
- [API Reference](https://valid8r.readthedocs.io/en/latest/api.html)
|
|
248
|
+
- [Parser Guide](https://valid8r.readthedocs.io/en/latest/parsers.html)
|
|
249
|
+
- [Validator Guide](https://valid8r.readthedocs.io/en/latest/validators.html)
|
|
250
|
+
- [Testing Guide](https://valid8r.readthedocs.io/en/latest/testing.html)
|
|
251
|
+
|
|
252
|
+
## Contributing
|
|
253
|
+
|
|
254
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
255
|
+
|
|
256
|
+
**Quick links**:
|
|
257
|
+
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
258
|
+
- [Development Setup](CONTRIBUTING.md#development-setup)
|
|
259
|
+
- [Commit Message Format](CONTRIBUTING.md#commit-messages)
|
|
260
|
+
- [Pull Request Process](CONTRIBUTING.md#pull-request-process)
|
|
261
|
+
|
|
262
|
+
### Development Quick Start
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# Install uv (fast dependency manager)
|
|
266
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
267
|
+
|
|
268
|
+
# Clone and install dependencies
|
|
269
|
+
git clone https://github.com/mikelane/valid8r
|
|
270
|
+
cd valid8r
|
|
271
|
+
uv sync
|
|
272
|
+
|
|
273
|
+
# Run tests
|
|
274
|
+
uv run tox
|
|
275
|
+
|
|
276
|
+
# Run linters
|
|
277
|
+
uv run ruff check .
|
|
278
|
+
uv run ruff format .
|
|
279
|
+
uv run mypy valid8r
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Project Status
|
|
283
|
+
|
|
284
|
+
Valid8r is in active development (v0.7.x). The API is stabilizing but may change before v1.0.0.
|
|
285
|
+
|
|
286
|
+
- ✅ Core parsers and validators
|
|
287
|
+
- ✅ Maybe monad error handling
|
|
288
|
+
- ✅ Interactive prompting
|
|
289
|
+
- ✅ Network parsers (URL, Email, IP, Phone)
|
|
290
|
+
- ✅ Collection parsers
|
|
291
|
+
- ✅ Comprehensive testing utilities
|
|
292
|
+
- 🚧 Additional validators (in progress)
|
|
293
|
+
- 🚧 Custom error types (planned)
|
|
294
|
+
|
|
295
|
+
See [ROADMAP.md](ROADMAP.md) for planned features.
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
300
|
+
|
|
301
|
+
Copyright (c) 2025 Mike Lane
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
**Made with ❤️ for the Python community**
|
valid8r-0.7.4/README.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Valid8r
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/valid8r/)
|
|
4
|
+
[](https://pypi.org/project/valid8r/)
|
|
5
|
+
[](https://github.com/mikelane/valid8r/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/mikelane/valid8r/actions)
|
|
7
|
+
[](https://codecov.io/gh/mikelane/valid8r)
|
|
8
|
+
[](https://valid8r.readthedocs.io/)
|
|
9
|
+
|
|
10
|
+
**Clean, composable input validation for Python using functional programming patterns.**
|
|
11
|
+
|
|
12
|
+
Valid8r makes input validation elegant and type-safe by using the Maybe monad for error handling. No more try-except blocks or boolean validation chains—just clean, composable parsers that tell you exactly what went wrong.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from valid8r import parsers, validators, prompt
|
|
16
|
+
|
|
17
|
+
# Parse and validate user input with rich error messages
|
|
18
|
+
age = prompt.ask(
|
|
19
|
+
"Enter your age: ",
|
|
20
|
+
parser=parsers.parse_int,
|
|
21
|
+
validator=validators.minimum(0) & validators.maximum(120)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
print(f"Your age is {age}")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Why Valid8r?
|
|
28
|
+
|
|
29
|
+
**Type-Safe Parsing**: Every parser returns `Maybe[T]` (Success or Failure), making error handling explicit and composable.
|
|
30
|
+
|
|
31
|
+
**Rich Structured Results**: Network parsers return dataclasses with parsed components—no more manual URL/email splitting.
|
|
32
|
+
|
|
33
|
+
**Chainable Validators**: Combine validators using `&` (and), `|` (or), and `~` (not) operators for complex validation logic.
|
|
34
|
+
|
|
35
|
+
**Zero Dependencies**: Core library uses only Python's standard library (with optional `uuid-utils` for faster UUID parsing).
|
|
36
|
+
|
|
37
|
+
**Interactive Prompts**: Built-in user input prompting with automatic retry and validation.
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install valid8r
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Requirements**: Python 3.11 or higher
|
|
48
|
+
|
|
49
|
+
### Basic Parsing
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from valid8r import parsers
|
|
53
|
+
from valid8r.core.maybe import Success, Failure
|
|
54
|
+
|
|
55
|
+
# Parse integers with automatic error handling
|
|
56
|
+
match parsers.parse_int("42"):
|
|
57
|
+
case Success(value):
|
|
58
|
+
print(f"Parsed: {value}") # Parsed: 42
|
|
59
|
+
case Failure(error):
|
|
60
|
+
print(f"Error: {error}")
|
|
61
|
+
|
|
62
|
+
# Parse dates (ISO 8601 format)
|
|
63
|
+
result = parsers.parse_date("2025-01-15")
|
|
64
|
+
assert result.is_success()
|
|
65
|
+
|
|
66
|
+
# Parse UUIDs with version validation
|
|
67
|
+
result = parsers.parse_uuid("550e8400-e29b-41d4-a716-446655440000", version=4)
|
|
68
|
+
assert result.is_success()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Validation with Combinators
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from valid8r import validators
|
|
75
|
+
|
|
76
|
+
# Combine validators using operators
|
|
77
|
+
age_validator = validators.minimum(0) & validators.maximum(120)
|
|
78
|
+
result = age_validator(42)
|
|
79
|
+
assert result.is_success()
|
|
80
|
+
|
|
81
|
+
# String validation
|
|
82
|
+
password_validator = (
|
|
83
|
+
validators.length(8, 128) &
|
|
84
|
+
validators.matches_regex(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]')
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Set validation
|
|
88
|
+
tags_validator = validators.subset_of({'python', 'rust', 'go', 'typescript'})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Structured Network Parsing
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from valid8r import parsers
|
|
95
|
+
|
|
96
|
+
# Parse URLs into structured components
|
|
97
|
+
match parsers.parse_url("https://user:pass@example.com:8443/path?query=1#fragment"):
|
|
98
|
+
case Success(url):
|
|
99
|
+
print(f"Scheme: {url.scheme}") # https
|
|
100
|
+
print(f"Host: {url.host}") # example.com
|
|
101
|
+
print(f"Port: {url.port}") # 8443
|
|
102
|
+
print(f"Path: {url.path}") # /path
|
|
103
|
+
print(f"Query: {url.query}") # {'query': '1'}
|
|
104
|
+
print(f"Fragment: {url.fragment}") # fragment
|
|
105
|
+
|
|
106
|
+
# Parse emails with normalized domains
|
|
107
|
+
match parsers.parse_email("User@Example.COM"):
|
|
108
|
+
case Success(email):
|
|
109
|
+
print(f"Local: {email.local}") # User
|
|
110
|
+
print(f"Domain: {email.domain}") # example.com (normalized)
|
|
111
|
+
|
|
112
|
+
# Parse phone numbers (NANP format)
|
|
113
|
+
match parsers.parse_phone("+1 (415) 555-2671"):
|
|
114
|
+
case Success(phone):
|
|
115
|
+
print(f"E.164: {phone.e164}") # +14155552671
|
|
116
|
+
print(f"National: {phone.national}") # (415) 555-2671
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Collection Parsing
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from valid8r import parsers
|
|
123
|
+
|
|
124
|
+
# Parse lists with element validation
|
|
125
|
+
result = parsers.parse_list("1,2,3,4,5", element_parser=parsers.parse_int)
|
|
126
|
+
assert result.value_or([]) == [1, 2, 3, 4, 5]
|
|
127
|
+
|
|
128
|
+
# Parse dictionaries with key/value parsers
|
|
129
|
+
result = parsers.parse_dict(
|
|
130
|
+
"name=Alice,age=30",
|
|
131
|
+
key_parser=lambda x: Success(x),
|
|
132
|
+
value_parser=lambda x: parsers.parse_int(x) if x.isdigit() else Success(x)
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Interactive Prompting
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from valid8r import prompt, parsers, validators
|
|
140
|
+
|
|
141
|
+
# Prompt with validation and automatic retry
|
|
142
|
+
email = prompt.ask(
|
|
143
|
+
"Email address: ",
|
|
144
|
+
parser=parsers.parse_email,
|
|
145
|
+
retry=2 # Retry twice on invalid input
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Combine parsing and validation
|
|
149
|
+
port = prompt.ask(
|
|
150
|
+
"Server port: ",
|
|
151
|
+
parser=parsers.parse_int,
|
|
152
|
+
validator=validators.between(1024, 65535),
|
|
153
|
+
retry=3
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Features
|
|
158
|
+
|
|
159
|
+
### Parsers
|
|
160
|
+
|
|
161
|
+
**Basic Types**:
|
|
162
|
+
- `parse_int`, `parse_float`, `parse_bool`, `parse_decimal`, `parse_complex`
|
|
163
|
+
- `parse_date` (ISO 8601), `parse_uuid` (with version validation)
|
|
164
|
+
|
|
165
|
+
**Collections**:
|
|
166
|
+
- `parse_list`, `parse_dict`, `parse_set` (with element parsers)
|
|
167
|
+
|
|
168
|
+
**Network & Communication**:
|
|
169
|
+
- `parse_ipv4`, `parse_ipv6`, `parse_ip`, `parse_cidr`
|
|
170
|
+
- `parse_url` → `UrlParts` (structured URL components)
|
|
171
|
+
- `parse_email` → `EmailAddress` (normalized domain)
|
|
172
|
+
- `parse_phone` → `PhoneNumber` (NANP validation with E.164 formatting)
|
|
173
|
+
|
|
174
|
+
**Advanced**:
|
|
175
|
+
- `parse_enum` (type-safe enum parsing)
|
|
176
|
+
- `create_parser`, `make_parser`, `validated_parser` (custom parser factories)
|
|
177
|
+
|
|
178
|
+
### Validators
|
|
179
|
+
|
|
180
|
+
**Numeric**: `minimum`, `maximum`, `between`
|
|
181
|
+
|
|
182
|
+
**String**: `non_empty_string`, `matches_regex`, `length`
|
|
183
|
+
|
|
184
|
+
**Collection**: `in_set`, `unique_items`, `subset_of`, `superset_of`, `is_sorted`
|
|
185
|
+
|
|
186
|
+
**Custom**: `predicate` (create validators from any function)
|
|
187
|
+
|
|
188
|
+
**Combinators**: Combine validators using `&` (and), `|` (or), `~` (not)
|
|
189
|
+
|
|
190
|
+
### Testing Utilities
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from valid8r.testing import (
|
|
194
|
+
assert_maybe_success,
|
|
195
|
+
assert_maybe_failure,
|
|
196
|
+
MockInputContext,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Test validation logic
|
|
200
|
+
result = validators.minimum(0)(42)
|
|
201
|
+
assert assert_maybe_success(result, 42)
|
|
202
|
+
|
|
203
|
+
result = validators.minimum(0)(-5)
|
|
204
|
+
assert assert_maybe_failure(result, "at least 0")
|
|
205
|
+
|
|
206
|
+
# Mock user input for testing prompts
|
|
207
|
+
with MockInputContext(["invalid", "valid@example.com"]):
|
|
208
|
+
result = prompt.ask("Email: ", parser=parsers.parse_email, retry=1)
|
|
209
|
+
assert result.is_success()
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Documentation
|
|
213
|
+
|
|
214
|
+
**Full documentation**: [valid8r.readthedocs.io](https://valid8r.readthedocs.io/)
|
|
215
|
+
|
|
216
|
+
- [API Reference](https://valid8r.readthedocs.io/en/latest/api.html)
|
|
217
|
+
- [Parser Guide](https://valid8r.readthedocs.io/en/latest/parsers.html)
|
|
218
|
+
- [Validator Guide](https://valid8r.readthedocs.io/en/latest/validators.html)
|
|
219
|
+
- [Testing Guide](https://valid8r.readthedocs.io/en/latest/testing.html)
|
|
220
|
+
|
|
221
|
+
## Contributing
|
|
222
|
+
|
|
223
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
224
|
+
|
|
225
|
+
**Quick links**:
|
|
226
|
+
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
|
227
|
+
- [Development Setup](CONTRIBUTING.md#development-setup)
|
|
228
|
+
- [Commit Message Format](CONTRIBUTING.md#commit-messages)
|
|
229
|
+
- [Pull Request Process](CONTRIBUTING.md#pull-request-process)
|
|
230
|
+
|
|
231
|
+
### Development Quick Start
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# Install uv (fast dependency manager)
|
|
235
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
236
|
+
|
|
237
|
+
# Clone and install dependencies
|
|
238
|
+
git clone https://github.com/mikelane/valid8r
|
|
239
|
+
cd valid8r
|
|
240
|
+
uv sync
|
|
241
|
+
|
|
242
|
+
# Run tests
|
|
243
|
+
uv run tox
|
|
244
|
+
|
|
245
|
+
# Run linters
|
|
246
|
+
uv run ruff check .
|
|
247
|
+
uv run ruff format .
|
|
248
|
+
uv run mypy valid8r
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Project Status
|
|
252
|
+
|
|
253
|
+
Valid8r is in active development (v0.7.x). The API is stabilizing but may change before v1.0.0.
|
|
254
|
+
|
|
255
|
+
- ✅ Core parsers and validators
|
|
256
|
+
- ✅ Maybe monad error handling
|
|
257
|
+
- ✅ Interactive prompting
|
|
258
|
+
- ✅ Network parsers (URL, Email, IP, Phone)
|
|
259
|
+
- ✅ Collection parsers
|
|
260
|
+
- ✅ Comprehensive testing utilities
|
|
261
|
+
- 🚧 Additional validators (in progress)
|
|
262
|
+
- 🚧 Custom error types (planned)
|
|
263
|
+
|
|
264
|
+
See [ROADMAP.md](ROADMAP.md) for planned features.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
269
|
+
|
|
270
|
+
Copyright (c) 2025 Mike Lane
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
**Made with ❤️ for the Python community**
|