valid8r.core
Core validation components.
Submodules
Classes
Structured email address. |
|
Structured URL components. |
Functions
|
Parse a CIDR network string (IPv4 or IPv6). |
|
Parse a bare email address of the form |
|
Parse a string as either an IPv4 or IPv6 address. |
|
Parse an IPv4 address string. |
|
Parse an IPv6 address string. |
|
Parse a URL with light validation. |
|
Generate a parser from a Python type annotation. |
Package Contents
- class valid8r.core.EmailAddress[source]
Structured email address.
- local
Local part (preserves original case).
- domain
Domain part lowercased.
Examples
>>> from valid8r.core.maybe import Success >>> match parse_email('First.Last+tag@Example.COM'): ... case Success(addr): ... (addr.local, addr.domain) ... case _: ... () ('First.Last+tag', 'example.com')
- class valid8r.core.UrlParts[source]
Structured URL components.
- scheme
Lowercased scheme (e.g. “http”).
- username
Username from userinfo, if present.
- password
Password from userinfo, if present.
- host
Lowercased host or IPv6 literal without brackets, or None when not provided and not required.
- port
Explicit port if present, otherwise None.
- path
Path component as-is (no normalization).
- query
Query string without leading ‘?’.
- fragment
Fragment without leading ‘#’.
Examples
>>> from valid8r.core.maybe import Success >>> match parse_url('https://alice:pw@example.com:8443/x?q=1#top'): ... case Success(u): ... (u.scheme, u.username, u.password, u.host, u.port, u.path, u.query, u.fragment) ... case _: ... () ('https', 'alice', 'pw', 'example.com', 8443, '/x', 'q=1', 'top')
- valid8r.core.parse_cidr(text, *, strict=True)[source]
Parse a CIDR network string (IPv4 or IPv6).
Validates and parses network addresses in CIDR notation (e.g., 192.168.1.0/24). By default, validates that host bits are not set (strict mode). With strict=False, host bits are masked to the network address.
- Parameters:
- Returns:
- Success with IPv4Network or IPv6Network if valid,
Failure(str) with error message otherwise
- Return type:
Maybe[IPv4Network | IPv6Network]
Examples
>>> parse_cidr("192.168.1.0/24") Success(IPv4Network('192.168.1.0/24')) >>> parse_cidr("10.0.0.0/8") Success(IPv4Network('10.0.0.0/8')) >>> parse_cidr("2001:db8::/32") Success(IPv6Network('2001:db8::/32')) >>> # Strict mode rejects host bits >>> parse_cidr("192.168.1.5/24").is_failure() True >>> # Non-strict mode masks host bits >>> result = parse_cidr("192.168.1.5/24", strict=False) >>> str(result.value_or(None)) '192.168.1.0/24'
- valid8r.core.parse_email(text)[source]
Parse a bare email address of the form
local@domain.Uses the email-validator library for RFC 5322 compliant validation. Domain names are normalized to lowercase, local parts preserve their case.
Requires the email-validator library to be installed. If not available, returns a Failure indicating the library is required.
Rules: - Trim surrounding whitespace - Full RFC 5322 email validation - Supports internationalized domains (IDNA) - Domain is lowercased in the result; local part preserves case
Failure messages: - Input must be a string - Input must not be empty - email-validator library is required but not installed - Various RFC-compliant validation error messages from email-validator
- Parameters:
text (str) – The email address string to parse
- Returns:
Success with EmailAddress or Failure with error message
- Return type:
Examples
>>> from valid8r.core.parsers import parse_email >>> from valid8r.core.maybe import Success >>> >>> # Parse an email with case normalization >>> result = parse_email('User.Name+tag@Example.COM') >>> isinstance(result, Success) True >>> email = result.value >>> # Local part preserves original case >>> email.local 'User.Name+tag' >>> # Domain is normalized to lowercase >>> email.domain 'example.com'
- valid8r.core.parse_ip(text)[source]
Parse a string as either an IPv4 or IPv6 address.
Automatically detects and parses either IPv4 or IPv6 addresses. Trims surrounding whitespace.
- Parameters:
text (str) – String containing an IP address (IPv4 or IPv6, whitespace is stripped)
- Returns:
- Success with IPv4Address or IPv6Address if valid,
Failure(str) with error message otherwise
- Return type:
Maybe[IPv4Address | IPv6Address]
Examples
>>> result = parse_ip("192.168.1.1") >>> result.is_success() True >>> result = parse_ip("::1") >>> result.is_success() True >>> parse_ip(" 10.0.0.1 ") Success(IPv4Address('10.0.0.1')) >>> parse_ip("not an ip").is_failure() True
- valid8r.core.parse_ipv4(text)[source]
Parse an IPv4 address string.
Validates and parses IPv4 addresses in dotted-decimal notation. Trims surrounding whitespace.
- Parameters:
text (str) – String containing an IPv4 address (whitespace is stripped)
- Returns:
Success(IPv4Address) if valid, Failure(str) with error message otherwise
- Return type:
Maybe[IPv4Address]
Examples
>>> parse_ipv4("192.168.1.1") Success(IPv4Address('192.168.1.1')) >>> parse_ipv4(" 10.0.0.1 ") Success(IPv4Address('10.0.0.1')) >>> parse_ipv4("256.1.1.1").is_failure() True >>> parse_ipv4("not an ip").is_failure() True
- valid8r.core.parse_ipv6(text)[source]
Parse an IPv6 address string.
Validates and parses IPv6 addresses in standard notation. Rejects scope IDs (e.g., %eth0). Trims surrounding whitespace.
- Parameters:
text (str) – String containing an IPv6 address (whitespace is stripped)
- Returns:
Success(IPv6Address) if valid, Failure(str) with error message otherwise
- Return type:
Maybe[IPv6Address]
Examples
>>> parse_ipv6("::1") Success(IPv6Address('::1')) >>> parse_ipv6("2001:0db8:85a3::8a2e:0370:7334") Success(IPv6Address('2001:db8:85a3::8a2e:370:7334')) >>> parse_ipv6(" fe80::1 ") Success(IPv6Address('fe80::1')) >>> parse_ipv6("192.168.1.1").is_failure() True
- valid8r.core.parse_url(text, *, allowed_schemes=('http', 'https'), require_host=True)[source]
Parse a URL with light validation.
Rules: - Trim surrounding whitespace only - Require scheme in allowed_schemes (defaults to http/https) - If require_host, netloc must include a valid host (hostname, IPv4, or bracketed IPv6) - Lowercase scheme and host; do not modify path/query/fragment
Failure messages (exact substrings): - Input must be a string - Input must not be empty - Unsupported URL scheme - URL requires host - Invalid host
- Parameters:
text (str) – The URL string to parse
allowed_schemes (collections.abc.Iterable[str]) – Iterable of allowed scheme names (default: (‘http’, ‘https’))
require_host (bool) – Whether to require a host in the URL (default: True)
- Returns:
Success with UrlParts containing parsed components, or Failure with error message
- Return type:
Examples
>>> from valid8r.core.parsers import parse_url >>> from valid8r.core.maybe import Success >>> >>> # Parse a complete URL >>> result = parse_url('https://user:pass@api.example.com:8080/v1/users?active=true#section') >>> isinstance(result, Success) True >>> url = result.value >>> url.scheme 'https' >>> url.host 'api.example.com' >>> url.port 8080 >>> url.path '/v1/users' >>> url.query 'active=true' >>> url.fragment 'section' >>> >>> # Access credentials >>> url.username 'user' >>> url.password 'pass'
- valid8r.core.from_type(annotation)[source]
Generate a parser from a Python type annotation.
This function uses match/case pattern matching to introspect type annotations and automatically generate appropriate parser functions. Supports basic types, generics, unions, literals, enums, and nested structures.
- Parameters:
annotation (type[T] | Any) – A Python type annotation (int, str, Optional[int], list[str], etc.)
- Returns:
A parser function that takes a string and returns Maybe[T]
- Raises:
ValueError – If annotation is None or unsupported type
TypeError – If annotation is not a valid type
- Return type:
collections.abc.Callable[[str], valid8r.core.maybe.Maybe[T]]
- Supported Types:
Basic types: int, str, float, bool
Optional types: Optional[T] treats empty string as None
Collections: list[T], dict[K,V], set[T] (with element validation)
Union types: Union[int, str] tries alternatives in order
Literal types: Literal[‘red’, ‘green’, ‘blue’] restricts to specific values
Enum types: Python Enum classes with case-insensitive matching
Annotated types: Annotated[int, validators.minimum(0)] chains validators
Nested types: list[dict[str, int]], dict[str, list[int]], etc.
Examples
Basic type parsing:
>>> from valid8r.core.type_adapters import from_type >>> parser = from_type(int) >>> result = parser('42') >>> result.value_or(None) 42
Optional type handling:
>>> from typing import Optional, Union >>> parser = from_type(Optional[int]) >>> parser('').value_or('not none') # Empty string becomes None >>> parser('42').value_or(None) 42
Collection parsing with validation:
>>> parser = from_type(list[int]) >>> result = parser('[1, 2, 3]') >>> result.value_or([]) [1, 2, 3] >>> parser('[1, "invalid", 3]').is_failure() True
Dictionary with typed keys and values:
>>> parser = from_type(dict[str, int]) >>> result = parser('{"age": 30, "count": 5}') >>> result.value_or({}) {'age': 30, 'count': 5}
Union types try alternatives:
>>> parser = from_type(Union[int, float, str]) >>> parser('42').value_or(None) # Parses as int 42 >>> parser('3.14').value_or(None) # Parses as float 3.14 >>> parser('hello').value_or(None) # Parses as str 'hello'
Literal types restrict values:
>>> from typing import Literal >>> parser = from_type(Literal['red', 'green', 'blue']) >>> parser('red').value_or(None) 'red' >>> parser('yellow').is_failure() True
Enum types with case-insensitive matching:
>>> from enum import Enum >>> class Status(Enum): ... ACTIVE = 'active' ... INACTIVE = 'inactive' >>> parser = from_type(Status) >>> parser('ACTIVE').value_or(None) <Status.ACTIVE: 'active'> >>> parser('active').value_or(None) # Case-insensitive <Status.ACTIVE: 'active'>
Annotated types with validators:
>>> from typing import Annotated >>> from valid8r import validators >>> parser = from_type(Annotated[int, validators.minimum(0), validators.maximum(100)]) >>> parser('50').value_or(None) 50 >>> parser('150').is_failure() # Exceeds maximum True >>> parser('-5').is_failure() # Below minimum True
Nested structures:
>>> parser = from_type(dict[str, list[int]]) >>> result = parser('{"scores": [95, 87, 92]}') >>> result.value_or({}) {'scores': [95, 87, 92]}
Notes
Collection parsers expect JSON format: ‘[1, 2, 3]’ for lists, ‘{“key”: “value”}’ for dicts
Nested structures are fully validated at each level
Union types return the first successful parse (order matters)
Enum matching is case-insensitive by default
Annotated validators are chained using bind() for composition