valid8r.core

Core validation components.

Submodules

Classes

EmailAddress

Structured email address.

UrlParts

Structured URL components.

Functions

parse_cidr(text, *[, strict])

Parse a CIDR network string (IPv4 or IPv6).

parse_email(text)

Parse a bare email address of the form local@domain.

parse_ip(text)

Parse a string as either an IPv4 or IPv6 address.

parse_ipv4(text)

Parse an IPv4 address string.

parse_ipv6(text)

Parse an IPv6 address string.

parse_url(text, *[, allowed_schemes, require_host])

Parse a URL with light validation.

from_type(annotation)

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')
local: str
domain: str
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')
scheme: str
username: str | None
password: str | None
host: str | None
port: int | None
path: str
query: str
fragment: str
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:
  • text (str) – String containing a CIDR network (whitespace is stripped)

  • strict (bool) – If True, reject networks with host bits set; if False, mask them (default: True)

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:

Maybe[EmailAddress]

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:

Maybe[UrlParts]

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