valid8r


Namevalid8r JSON
Version 0.7.0 PyPI version JSON
download
home_pageNone
SummaryClean, flexible input validation for Python applications
upload_time2025-10-31 05:16:18
maintainerNone
docs_urlNone
authorMike Lane
requires_python<4.0,>=3.11
licenseMIT
keywords validation input cli maybe-monad parsing functional-programming
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage
            # Valid8r

[![PyPI version](https://img.shields.io/pypi/v/valid8r.svg)](https://pypi.org/project/valid8r/)
[![Python versions](https://img.shields.io/pypi/pyversions/valid8r.svg)](https://pypi.org/project/valid8r/)
[![License](https://img.shields.io/github/license/mikelane/valid8r.svg)](https://github.com/mikelane/valid8r/blob/main/LICENSE)
[![CI Status](https://img.shields.io/github/actions/workflow/status/mikelane/valid8r/ci.yml?branch=main)](https://github.com/mikelane/valid8r/actions)
[![codecov](https://codecov.io/gh/mikelane/valid8r/branch/main/graph/badge.svg)](https://codecov.io/gh/mikelane/valid8r)
[![Documentation](https://img.shields.io/readthedocs/valid8r.svg)](https://valid8r.readthedocs.io/)

[![PyPI downloads](https://img.shields.io/pypi/dm/valid8r.svg)](https://pypi.org/project/valid8r/)
[![GitHub stars](https://img.shields.io/github/stars/mikelane/valid8r.svg)](https://github.com/mikelane/valid8r/stargazers)
[![GitHub contributors](https://img.shields.io/github/contributors/mikelane/valid8r.svg)](https://github.com/mikelane/valid8r/graphs/contributors)
[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
[![Semantic Release](https://img.shields.io/badge/semantic--release-python-blue)](https://github.com/python-semantic-release/python-semantic-release)

A clean, flexible input validation library for Python applications.

## Features

- **Clean Type Parsing**: Parse strings to various Python types with robust error handling
- **Flexible Validation**: Chain validators and create custom validation rules
- **Monadic Error Handling**: Use Maybe monad for clean error propagation
- **Input Prompting**: Prompt users for input with built-in validation
- **Structured Results**: Network parsers return rich dataclasses with parsed components

## Available Parsers

### Basic Types
- **Numbers**: `parse_int`, `parse_float`, `parse_complex`, `parse_decimal`
- **Text**: `parse_bool` (flexible true/false parsing)
- **Dates**: `parse_date` (ISO 8601 format)
- **UUIDs**: `parse_uuid` (with optional version validation)

### Collections
- **Lists**: `parse_list` (with element parser)
- **Dictionaries**: `parse_dict` (with key/value parsers)
- **Sets**: `parse_set` (with element parser)

### Network & Communication
- **IP Addresses**: `parse_ipv4`, `parse_ipv6`, `parse_ip` (either v4 or v6)
- **Networks**: `parse_cidr` (IPv4/IPv6 CIDR notation)
- **Phone Numbers**: `parse_phone` → PhoneNumber (NANP validation)
- **URLs**: `parse_url` → UrlParts (scheme, host, port, path, query, etc.)
- **Email**: `parse_email` → EmailAddress (normalized case)

### Advanced
- **Enums**: `parse_enum` (type-safe enum parsing)
- **Custom**: `create_parser`, `make_parser`, `validated_parser` (parser factories)

## Available Validators

### Numeric Validators
- **`minimum(min_value)`** - Ensures value is at least the minimum (inclusive)
- **`maximum(max_value)`** - Ensures value is at most the maximum (inclusive)
- **`between(min_value, max_value)`** - Ensures value is within range (inclusive)

### String Validators
- **`non_empty_string()`** - Rejects empty strings and whitespace-only strings
- **`matches_regex(pattern)`** - Validates string matches regex pattern (string or compiled)
- **`length(min_length, max_length)`** - Validates string length within bounds

### Collection Validators
- **`in_set(allowed_values)`** - Ensures value is in set of allowed values
- **`unique_items()`** - Ensures all items in a list are unique
- **`subset_of(allowed_set)`** - Validates set is subset of allowed values
- **`superset_of(required_set)`** - Validates set is superset of required values
- **`is_sorted(reverse=False)`** - Ensures list is sorted (ascending or descending)

### Custom Validators
- **`predicate(func, error_message)`** - Create custom validator from any predicate function

**Note**: All validators support custom error messages and can be combined using `&` (and), `|` (or), and `~` (not) operators.

## Installation

**Requirements**: Python 3.11 or higher

```bash
pip install valid8r
```

Valid8r supports Python 3.11, 3.12, 3.13, and 3.14.

## Quick Start

```python
from valid8r import (
    parsers,
    prompt,
    validators,
)

# Simple validation
age = prompt.ask(
    "Enter your age: ",
    parser=parsers.parse_int,
    validator=validators.minimum(0) & validators.maximum(120)
)

print(f"Your age is {age}")
```

### IP parsing helpers

```python
from valid8r.core.maybe import Success, Failure
from valid8r.core import parsers

# IPv4 / IPv6 / generic IP
for text in ["192.168.0.1", "::1", " 10.0.0.1 "]:
    match parsers.parse_ip(text):
        case Success(addr):
            print("Parsed:", addr)
        case Failure(err):
            print("Error:", err)

# CIDR (strict by default)
match parsers.parse_cidr("10.0.0.0/8"):
    case Success(net):
        print("Network:", net)  # 10.0.0.0/8
    case Failure(err):
        print("Error:", err)

# Non-strict masks host bits
match parsers.parse_cidr("10.0.0.1/24", strict=False):
    case Success(net):
        assert str(net) == "10.0.0.0/24"
```

### URL and Email helpers

```python
from valid8r.core.maybe import Success, Failure
from valid8r.core import parsers

# URL parsing with structured result (UrlParts)
match parsers.parse_url("https://alice:pw@example.com:8443/x?q=1#top"):
    case Success(u):
        print(f"Scheme: {u.scheme}")     # https
        print(f"Host: {u.host}")         # example.com
        print(f"Port: {u.port}")         # 8443
        print(f"Path: {u.path}")         # /x
        print(f"Query: {u.query}")       # {'q': '1'}
        print(f"Fragment: {u.fragment}") # top
    case Failure(err):
        print("Error:", err)

# Email parsing with normalized case (EmailAddress)
match parsers.parse_email("First.Last+tag@Example.COM"):
    case Success(e):
        print(f"Local: {e.local}")   # First.Last+tag
        print(f"Domain: {e.domain}") # example.com (normalized)
    case Failure(err):
        print("Error:", err)
```

### Phone Number Parsing

```python
from valid8r.core.maybe import Success, Failure
from valid8r.core import parsers

# Phone number parsing with NANP validation (PhoneNumber)
match parsers.parse_phone("+1 (415) 555-2671"):
    case Success(phone):
        print(f"Country: {phone.country_code}")  # 1
        print(f"Area: {phone.area_code}")        # 415
        print(f"Exchange: {phone.exchange}")     # 555
        print(f"Subscriber: {phone.subscriber}") # 2671

        # Format for display using properties
        print(f"E.164: {phone.e164}")           # +14155552671
        print(f"National: {phone.national}")    # (415) 555-2671
    case Failure(err):
        print("Error:", err)

# Also accepts various formats
for number in ["4155552671", "(415) 555-2671", "415-555-2671"]:
    result = parsers.parse_phone(number)
    assert result.is_success()
```

## Testing Support

Valid8r includes comprehensive testing utilities to help you verify your validation logic:

```python
from valid8r import Maybe, validators, parsers, prompt
from valid8r.testing import (
    MockInputContext,
    assert_maybe_success,
    assert_maybe_failure,
)

def validate_age(age: int) -> Maybe[int]:
    """Validate age is between 0 and 120."""
    return (validators.minimum(0) & validators.maximum(120))(age)

# Test validation functions with assert helpers
result = validate_age(42)
assert assert_maybe_success(result, 42)

result = validate_age(-5)
assert assert_maybe_failure(result, "at least 0")

# Test prompts with mock input
with MockInputContext(["yes", "42", "invalid", "25"]):
    # First prompt - returns Maybe, unwrap with value_or()
    result = prompt.ask("Continue? ", parser=parsers.parse_bool)
    assert result.value_or(False) == True

    # Second prompt - unwrap the Maybe result
    result = prompt.ask(
        "Age? ",
        parser=parsers.parse_int,
        validator=validate_age
    )
    age = result.value_or(None)
    assert age == 42

    # Third prompt will fail, fourth succeeds - unwrap result
    result = prompt.ask(
        "Age again? ",
        parser=parsers.parse_int,
        retry=1  # Retry once after failure
    )
    age = result.value_or(None)
    assert age == 25
```

### Testing Utilities Reference

- **`assert_maybe_success(result, expected_value)`**: Assert that a Maybe is Success with the expected value
- **`assert_maybe_failure(result, error_substring)`**: Assert that a Maybe is Failure containing the error substring
- **`MockInputContext(inputs)`**: Context manager for mocking user input in tests

For more examples, see the [documentation](https://valid8r.readthedocs.io/).

## Development

This project uses Poetry for dependency management and Tox for testing.

### Setup

```bash
# Install Poetry
curl -sSL https://install.python-poetry.org | python3 -

# Install dependencies
poetry install
```

### Running Tests

```bash
# Run all tests
poetry run tox

# Run BDD tests
poetry run tox -e bdd
```

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

Copyright (c) 2025 Mike Lane


            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "valid8r",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.11",
    "maintainer_email": null,
    "keywords": "validation, input, cli, maybe-monad, parsing, functional-programming",
    "author": "Mike Lane",
    "author_email": "mikelane@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/b4/eb/39829e6123ca4a8aeca23653b120a0fbdad908919866284d1007bbcc1d6f/valid8r-0.7.0.tar.gz",
    "platform": null,
    "description": "# Valid8r\n\n[![PyPI version](https://img.shields.io/pypi/v/valid8r.svg)](https://pypi.org/project/valid8r/)\n[![Python versions](https://img.shields.io/pypi/pyversions/valid8r.svg)](https://pypi.org/project/valid8r/)\n[![License](https://img.shields.io/github/license/mikelane/valid8r.svg)](https://github.com/mikelane/valid8r/blob/main/LICENSE)\n[![CI Status](https://img.shields.io/github/actions/workflow/status/mikelane/valid8r/ci.yml?branch=main)](https://github.com/mikelane/valid8r/actions)\n[![codecov](https://codecov.io/gh/mikelane/valid8r/branch/main/graph/badge.svg)](https://codecov.io/gh/mikelane/valid8r)\n[![Documentation](https://img.shields.io/readthedocs/valid8r.svg)](https://valid8r.readthedocs.io/)\n\n[![PyPI downloads](https://img.shields.io/pypi/dm/valid8r.svg)](https://pypi.org/project/valid8r/)\n[![GitHub stars](https://img.shields.io/github/stars/mikelane/valid8r.svg)](https://github.com/mikelane/valid8r/stargazers)\n[![GitHub contributors](https://img.shields.io/github/contributors/mikelane/valid8r.svg)](https://github.com/mikelane/valid8r/graphs/contributors)\n[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)\n[![Semantic Release](https://img.shields.io/badge/semantic--release-python-blue)](https://github.com/python-semantic-release/python-semantic-release)\n\nA clean, flexible input validation library for Python applications.\n\n## Features\n\n- **Clean Type Parsing**: Parse strings to various Python types with robust error handling\n- **Flexible Validation**: Chain validators and create custom validation rules\n- **Monadic Error Handling**: Use Maybe monad for clean error propagation\n- **Input Prompting**: Prompt users for input with built-in validation\n- **Structured Results**: Network parsers return rich dataclasses with parsed components\n\n## Available Parsers\n\n### Basic Types\n- **Numbers**: `parse_int`, `parse_float`, `parse_complex`, `parse_decimal`\n- **Text**: `parse_bool` (flexible true/false parsing)\n- **Dates**: `parse_date` (ISO 8601 format)\n- **UUIDs**: `parse_uuid` (with optional version validation)\n\n### Collections\n- **Lists**: `parse_list` (with element parser)\n- **Dictionaries**: `parse_dict` (with key/value parsers)\n- **Sets**: `parse_set` (with element parser)\n\n### Network & Communication\n- **IP Addresses**: `parse_ipv4`, `parse_ipv6`, `parse_ip` (either v4 or v6)\n- **Networks**: `parse_cidr` (IPv4/IPv6 CIDR notation)\n- **Phone Numbers**: `parse_phone` \u2192 PhoneNumber (NANP validation)\n- **URLs**: `parse_url` \u2192 UrlParts (scheme, host, port, path, query, etc.)\n- **Email**: `parse_email` \u2192 EmailAddress (normalized case)\n\n### Advanced\n- **Enums**: `parse_enum` (type-safe enum parsing)\n- **Custom**: `create_parser`, `make_parser`, `validated_parser` (parser factories)\n\n## Available Validators\n\n### Numeric Validators\n- **`minimum(min_value)`** - Ensures value is at least the minimum (inclusive)\n- **`maximum(max_value)`** - Ensures value is at most the maximum (inclusive)\n- **`between(min_value, max_value)`** - Ensures value is within range (inclusive)\n\n### String Validators\n- **`non_empty_string()`** - Rejects empty strings and whitespace-only strings\n- **`matches_regex(pattern)`** - Validates string matches regex pattern (string or compiled)\n- **`length(min_length, max_length)`** - Validates string length within bounds\n\n### Collection Validators\n- **`in_set(allowed_values)`** - Ensures value is in set of allowed values\n- **`unique_items()`** - Ensures all items in a list are unique\n- **`subset_of(allowed_set)`** - Validates set is subset of allowed values\n- **`superset_of(required_set)`** - Validates set is superset of required values\n- **`is_sorted(reverse=False)`** - Ensures list is sorted (ascending or descending)\n\n### Custom Validators\n- **`predicate(func, error_message)`** - Create custom validator from any predicate function\n\n**Note**: All validators support custom error messages and can be combined using `&` (and), `|` (or), and `~` (not) operators.\n\n## Installation\n\n**Requirements**: Python 3.11 or higher\n\n```bash\npip install valid8r\n```\n\nValid8r supports Python 3.11, 3.12, 3.13, and 3.14.\n\n## Quick Start\n\n```python\nfrom valid8r import (\n    parsers,\n    prompt,\n    validators,\n)\n\n# Simple validation\nage = prompt.ask(\n    \"Enter your age: \",\n    parser=parsers.parse_int,\n    validator=validators.minimum(0) & validators.maximum(120)\n)\n\nprint(f\"Your age is {age}\")\n```\n\n### IP parsing helpers\n\n```python\nfrom valid8r.core.maybe import Success, Failure\nfrom valid8r.core import parsers\n\n# IPv4 / IPv6 / generic IP\nfor text in [\"192.168.0.1\", \"::1\", \" 10.0.0.1 \"]:\n    match parsers.parse_ip(text):\n        case Success(addr):\n            print(\"Parsed:\", addr)\n        case Failure(err):\n            print(\"Error:\", err)\n\n# CIDR (strict by default)\nmatch parsers.parse_cidr(\"10.0.0.0/8\"):\n    case Success(net):\n        print(\"Network:\", net)  # 10.0.0.0/8\n    case Failure(err):\n        print(\"Error:\", err)\n\n# Non-strict masks host bits\nmatch parsers.parse_cidr(\"10.0.0.1/24\", strict=False):\n    case Success(net):\n        assert str(net) == \"10.0.0.0/24\"\n```\n\n### URL and Email helpers\n\n```python\nfrom valid8r.core.maybe import Success, Failure\nfrom valid8r.core import parsers\n\n# URL parsing with structured result (UrlParts)\nmatch parsers.parse_url(\"https://alice:pw@example.com:8443/x?q=1#top\"):\n    case Success(u):\n        print(f\"Scheme: {u.scheme}\")     # https\n        print(f\"Host: {u.host}\")         # example.com\n        print(f\"Port: {u.port}\")         # 8443\n        print(f\"Path: {u.path}\")         # /x\n        print(f\"Query: {u.query}\")       # {'q': '1'}\n        print(f\"Fragment: {u.fragment}\") # top\n    case Failure(err):\n        print(\"Error:\", err)\n\n# Email parsing with normalized case (EmailAddress)\nmatch parsers.parse_email(\"First.Last+tag@Example.COM\"):\n    case Success(e):\n        print(f\"Local: {e.local}\")   # First.Last+tag\n        print(f\"Domain: {e.domain}\") # example.com (normalized)\n    case Failure(err):\n        print(\"Error:\", err)\n```\n\n### Phone Number Parsing\n\n```python\nfrom valid8r.core.maybe import Success, Failure\nfrom valid8r.core import parsers\n\n# Phone number parsing with NANP validation (PhoneNumber)\nmatch parsers.parse_phone(\"+1 (415) 555-2671\"):\n    case Success(phone):\n        print(f\"Country: {phone.country_code}\")  # 1\n        print(f\"Area: {phone.area_code}\")        # 415\n        print(f\"Exchange: {phone.exchange}\")     # 555\n        print(f\"Subscriber: {phone.subscriber}\") # 2671\n\n        # Format for display using properties\n        print(f\"E.164: {phone.e164}\")           # +14155552671\n        print(f\"National: {phone.national}\")    # (415) 555-2671\n    case Failure(err):\n        print(\"Error:\", err)\n\n# Also accepts various formats\nfor number in [\"4155552671\", \"(415) 555-2671\", \"415-555-2671\"]:\n    result = parsers.parse_phone(number)\n    assert result.is_success()\n```\n\n## Testing Support\n\nValid8r includes comprehensive testing utilities to help you verify your validation logic:\n\n```python\nfrom valid8r import Maybe, validators, parsers, prompt\nfrom valid8r.testing import (\n    MockInputContext,\n    assert_maybe_success,\n    assert_maybe_failure,\n)\n\ndef validate_age(age: int) -> Maybe[int]:\n    \"\"\"Validate age is between 0 and 120.\"\"\"\n    return (validators.minimum(0) & validators.maximum(120))(age)\n\n# Test validation functions with assert helpers\nresult = validate_age(42)\nassert assert_maybe_success(result, 42)\n\nresult = validate_age(-5)\nassert assert_maybe_failure(result, \"at least 0\")\n\n# Test prompts with mock input\nwith MockInputContext([\"yes\", \"42\", \"invalid\", \"25\"]):\n    # First prompt - returns Maybe, unwrap with value_or()\n    result = prompt.ask(\"Continue? \", parser=parsers.parse_bool)\n    assert result.value_or(False) == True\n\n    # Second prompt - unwrap the Maybe result\n    result = prompt.ask(\n        \"Age? \",\n        parser=parsers.parse_int,\n        validator=validate_age\n    )\n    age = result.value_or(None)\n    assert age == 42\n\n    # Third prompt will fail, fourth succeeds - unwrap result\n    result = prompt.ask(\n        \"Age again? \",\n        parser=parsers.parse_int,\n        retry=1  # Retry once after failure\n    )\n    age = result.value_or(None)\n    assert age == 25\n```\n\n### Testing Utilities Reference\n\n- **`assert_maybe_success(result, expected_value)`**: Assert that a Maybe is Success with the expected value\n- **`assert_maybe_failure(result, error_substring)`**: Assert that a Maybe is Failure containing the error substring\n- **`MockInputContext(inputs)`**: Context manager for mocking user input in tests\n\nFor more examples, see the [documentation](https://valid8r.readthedocs.io/).\n\n## Development\n\nThis project uses Poetry for dependency management and Tox for testing.\n\n### Setup\n\n```bash\n# Install Poetry\ncurl -sSL https://install.python-poetry.org | python3 -\n\n# Install dependencies\npoetry install\n```\n\n### Running Tests\n\n```bash\n# Run all tests\npoetry run tox\n\n# Run BDD tests\npoetry run tox -e bdd\n```\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\nCopyright (c) 2025 Mike Lane\n\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Clean, flexible input validation for Python applications",
    "version": "0.7.0",
    "project_urls": {
        "Documentation": "https://valid8r.readthedocs.io/",
        "Homepage": "https://valid8r.readthedocs.io/",
        "Repository": "https://github.com/mikelane/valid8r"
    },
    "split_keywords": [
        "validation",
        " input",
        " cli",
        " maybe-monad",
        " parsing",
        " functional-programming"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "d70940f25ebeab2aa642b4ce5ee409ce719a5353fd8ddc7464948836906a2d2b",
                "md5": "95c47814661ba3475545fab7281b7b56",
                "sha256": "f1d2a2aa963750b3d69ef80dc11a695e55cd83ed2585b5acf67c7a714c195e4b"
            },
            "downloads": -1,
            "filename": "valid8r-0.7.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "95c47814661ba3475545fab7281b7b56",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.11",
            "size": 34438,
            "upload_time": "2025-10-31T05:16:17",
            "upload_time_iso_8601": "2025-10-31T05:16:17.914578Z",
            "url": "https://files.pythonhosted.org/packages/d7/09/40f25ebeab2aa642b4ce5ee409ce719a5353fd8ddc7464948836906a2d2b/valid8r-0.7.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "b4eb39829e6123ca4a8aeca23653b120a0fbdad908919866284d1007bbcc1d6f",
                "md5": "27423238b45349b5522553dc92b05593",
                "sha256": "073b869a4aac60d0b098a11233fb09ce2cd82984b94f291378d8473668d6ffeb"
            },
            "downloads": -1,
            "filename": "valid8r-0.7.0.tar.gz",
            "has_sig": false,
            "md5_digest": "27423238b45349b5522553dc92b05593",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.11",
            "size": 34266,
            "upload_time": "2025-10-31T05:16:18",
            "upload_time_iso_8601": "2025-10-31T05:16:18.768859Z",
            "url": "https://files.pythonhosted.org/packages/b4/eb/39829e6123ca4a8aeca23653b120a0fbdad908919866284d1007bbcc1d6f/valid8r-0.7.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-31 05:16:18",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "mikelane",
    "github_project": "valid8r",
    "travis_ci": false,
    "coveralls": true,
    "github_actions": true,
    "tox": true,
    "lcname": "valid8r"
}
        
Elapsed time: 1.14442s