| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694 |
- """The networks module contains types for common network-related fields."""
- from __future__ import annotations as _annotations
- import dataclasses as _dataclasses
- import re
- from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
- from typing import TYPE_CHECKING, Any
- from pydantic_core import MultiHostUrl, PydanticCustomError, Url, core_schema
- from typing_extensions import Annotated, TypeAlias
- from ._internal import _fields, _repr, _schema_generation_shared
- from ._migration import getattr_migration
- from .annotated_handlers import GetCoreSchemaHandler
- from .json_schema import JsonSchemaValue
- if TYPE_CHECKING:
- import email_validator
- NetworkType: TypeAlias = 'str | bytes | int | tuple[str | bytes | int, str | int]'
- else:
- email_validator = None
- __all__ = [
- 'AnyUrl',
- 'AnyHttpUrl',
- 'FileUrl',
- 'HttpUrl',
- 'UrlConstraints',
- 'EmailStr',
- 'NameEmail',
- 'IPvAnyAddress',
- 'IPvAnyInterface',
- 'IPvAnyNetwork',
- 'PostgresDsn',
- 'CockroachDsn',
- 'AmqpDsn',
- 'RedisDsn',
- 'MongoDsn',
- 'KafkaDsn',
- 'validate_email',
- 'MySQLDsn',
- 'MariaDBDsn',
- ]
- @_dataclasses.dataclass
- class UrlConstraints(_fields.PydanticMetadata):
- """Url constraints.
- Attributes:
- max_length: The maximum length of the url. Defaults to `None`.
- allowed_schemes: The allowed schemes. Defaults to `None`.
- host_required: Whether the host is required. Defaults to `None`.
- default_host: The default host. Defaults to `None`.
- default_port: The default port. Defaults to `None`.
- default_path: The default path. Defaults to `None`.
- """
- max_length: int | None = None
- allowed_schemes: list[str] | None = None
- host_required: bool | None = None
- default_host: str | None = None
- default_port: int | None = None
- default_path: str | None = None
- def __hash__(self) -> int:
- return hash(
- (
- self.max_length,
- tuple(self.allowed_schemes) if self.allowed_schemes is not None else None,
- self.host_required,
- self.default_host,
- self.default_port,
- self.default_path,
- )
- )
- AnyUrl = Url
- """Base type for all URLs.
- * Any scheme allowed
- * Top-level domain (TLD) not required
- * Host required
- Assuming an input URL of `http://samuel:pass@example.com:8000/the/path/?query=here#fragment=is;this=bit`,
- the types export the following properties:
- - `scheme`: the URL scheme (`http`), always set.
- - `host`: the URL host (`example.com`), always set.
- - `username`: optional username if included (`samuel`).
- - `password`: optional password if included (`pass`).
- - `port`: optional port (`8000`).
- - `path`: optional path (`/the/path/`).
- - `query`: optional URL query (for example, `GET` arguments or "search string", such as `query=here`).
- - `fragment`: optional fragment (`fragment=is;this=bit`).
- """
- AnyHttpUrl = Annotated[Url, UrlConstraints(allowed_schemes=['http', 'https'])]
- """A type that will accept any http or https URL.
- * TLD not required
- * Host required
- """
- HttpUrl = Annotated[Url, UrlConstraints(max_length=2083, allowed_schemes=['http', 'https'])]
- """A type that will accept any http or https URL.
- * TLD required
- * Host required
- * Max length 2083
- ```py
- from pydantic import BaseModel, HttpUrl, ValidationError
- class MyModel(BaseModel):
- url: HttpUrl
- m = MyModel(url='http://www.example.com')
- print(m.url)
- #> http://www.example.com/
- try:
- MyModel(url='ftp://invalid.url')
- except ValidationError as e:
- print(e)
- '''
- 1 validation error for MyModel
- url
- URL scheme should be 'http' or 'https' [type=url_scheme, input_value='ftp://invalid.url', input_type=str]
- '''
- try:
- MyModel(url='not a url')
- except ValidationError as e:
- print(e)
- '''
- 1 validation error for MyModel
- url
- Input should be a valid URL, relative URL without a base [type=url_parsing, input_value='not a url', input_type=str]
- '''
- ```
- "International domains" (e.g. a URL where the host or TLD includes non-ascii characters) will be encoded via
- [punycode](https://en.wikipedia.org/wiki/Punycode) (see
- [this article](https://www.xudongz.com/blog/2017/idn-phishing/) for a good description of why this is important):
- ```py
- from pydantic import BaseModel, HttpUrl
- class MyModel(BaseModel):
- url: HttpUrl
- m1 = MyModel(url='http://puny£code.com')
- print(m1.url)
- #> http://xn--punycode-eja.com/
- m2 = MyModel(url='https://www.аррӏе.com/')
- print(m2.url)
- #> https://www.xn--80ak6aa92e.com/
- m3 = MyModel(url='https://www.example.珠宝/')
- print(m3.url)
- #> https://www.example.xn--pbt977c/
- ```
- !!! warning "Underscores in Hostnames"
- In Pydantic, underscores are allowed in all parts of a domain except the TLD.
- Technically this might be wrong - in theory the hostname cannot have underscores, but subdomains can.
- To explain this; consider the following two cases:
- - `exam_ple.co.uk`: the hostname is `exam_ple`, which should not be allowed since it contains an underscore.
- - `foo_bar.example.com` the hostname is `example`, which should be allowed since the underscore is in the subdomain.
- Without having an exhaustive list of TLDs, it would be impossible to differentiate between these two. Therefore
- underscores are allowed, but you can always do further validation in a validator if desired.
- Also, Chrome, Firefox, and Safari all currently accept `http://exam_ple.com` as a URL, so we're in good
- (or at least big) company.
- """
- FileUrl = Annotated[Url, UrlConstraints(allowed_schemes=['file'])]
- """A type that will accept any file URL.
- * Host not required
- """
- PostgresDsn = Annotated[
- MultiHostUrl,
- UrlConstraints(
- host_required=True,
- allowed_schemes=[
- 'postgres',
- 'postgresql',
- 'postgresql+asyncpg',
- 'postgresql+pg8000',
- 'postgresql+psycopg',
- 'postgresql+psycopg2',
- 'postgresql+psycopg2cffi',
- 'postgresql+py-postgresql',
- 'postgresql+pygresql',
- ],
- ),
- ]
- """A type that will accept any Postgres DSN.
- * User info required
- * TLD not required
- * Host required
- * Supports multiple hosts
- If further validation is required, these properties can be used by validators to enforce specific behaviour:
- ```py
- from pydantic import (
- BaseModel,
- HttpUrl,
- PostgresDsn,
- ValidationError,
- field_validator,
- )
- class MyModel(BaseModel):
- url: HttpUrl
- m = MyModel(url='http://www.example.com')
- # the repr() method for a url will display all properties of the url
- print(repr(m.url))
- #> Url('http://www.example.com/')
- print(m.url.scheme)
- #> http
- print(m.url.host)
- #> www.example.com
- print(m.url.port)
- #> 80
- class MyDatabaseModel(BaseModel):
- db: PostgresDsn
- @field_validator('db')
- def check_db_name(cls, v):
- assert v.path and len(v.path) > 1, 'database must be provided'
- return v
- m = MyDatabaseModel(db='postgres://user:pass@localhost:5432/foobar')
- print(m.db)
- #> postgres://user:pass@localhost:5432/foobar
- try:
- MyDatabaseModel(db='postgres://user:pass@localhost:5432')
- except ValidationError as e:
- print(e)
- '''
- 1 validation error for MyDatabaseModel
- db
- Assertion failed, database must be provided
- assert (None)
- + where None = MultiHostUrl('postgres://user:pass@localhost:5432').path [type=assertion_error, input_value='postgres://user:pass@localhost:5432', input_type=str]
- '''
- ```
- """
- CockroachDsn = Annotated[
- Url,
- UrlConstraints(
- host_required=True,
- allowed_schemes=[
- 'cockroachdb',
- 'cockroachdb+psycopg2',
- 'cockroachdb+asyncpg',
- ],
- ),
- ]
- """A type that will accept any Cockroach DSN.
- * User info required
- * TLD not required
- * Host required
- """
- AmqpDsn = Annotated[Url, UrlConstraints(allowed_schemes=['amqp', 'amqps'])]
- """A type that will accept any AMQP DSN.
- * User info required
- * TLD not required
- * Host required
- """
- RedisDsn = Annotated[
- Url,
- UrlConstraints(allowed_schemes=['redis', 'rediss'], default_host='localhost', default_port=6379, default_path='/0'),
- ]
- """A type that will accept any Redis DSN.
- * User info required
- * TLD not required
- * Host required (e.g., `rediss://:pass@localhost`)
- """
- MongoDsn = Annotated[MultiHostUrl, UrlConstraints(allowed_schemes=['mongodb', 'mongodb+srv'], default_port=27017)]
- """A type that will accept any MongoDB DSN.
- * User info not required
- * Database name not required
- * Port not required
- * User info may be passed without user part (e.g., `mongodb://mongodb0.example.com:27017`).
- """
- KafkaDsn = Annotated[Url, UrlConstraints(allowed_schemes=['kafka'], default_host='localhost', default_port=9092)]
- """A type that will accept any Kafka DSN.
- * User info required
- * TLD not required
- * Host required
- """
- MySQLDsn = Annotated[
- Url,
- UrlConstraints(
- allowed_schemes=[
- 'mysql',
- 'mysql+mysqlconnector',
- 'mysql+aiomysql',
- 'mysql+asyncmy',
- 'mysql+mysqldb',
- 'mysql+pymysql',
- 'mysql+cymysql',
- 'mysql+pyodbc',
- ],
- default_port=3306,
- ),
- ]
- """A type that will accept any MySQL DSN.
- * User info required
- * TLD not required
- * Host required
- """
- MariaDBDsn = Annotated[
- Url,
- UrlConstraints(
- allowed_schemes=['mariadb', 'mariadb+mariadbconnector', 'mariadb+pymysql'],
- default_port=3306,
- ),
- ]
- """A type that will accept any MariaDB DSN.
- * User info required
- * TLD not required
- * Host required
- """
- def import_email_validator() -> None:
- global email_validator
- try:
- import email_validator
- except ImportError as e:
- raise ImportError('email-validator is not installed, run `pip install pydantic[email]`') from e
- if TYPE_CHECKING:
- EmailStr = Annotated[str, ...]
- else:
- class EmailStr:
- """
- Info:
- To use this type, you need to install the optional
- [`email-validator`](https://github.com/JoshData/python-email-validator) package:
- ```bash
- pip install email-validator
- ```
- Validate email addresses.
- ```py
- from pydantic import BaseModel, EmailStr
- class Model(BaseModel):
- email: EmailStr
- print(Model(email='contact@mail.com'))
- #> email='contact@mail.com'
- ```
- """ # noqa: D212
- @classmethod
- def __get_pydantic_core_schema__(
- cls,
- _source: type[Any],
- _handler: GetCoreSchemaHandler,
- ) -> core_schema.CoreSchema:
- import_email_validator()
- return core_schema.no_info_after_validator_function(cls._validate, core_schema.str_schema())
- @classmethod
- def __get_pydantic_json_schema__(
- cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler
- ) -> JsonSchemaValue:
- field_schema = handler(core_schema)
- field_schema.update(type='string', format='email')
- return field_schema
- @classmethod
- def _validate(cls, __input_value: str) -> str:
- return validate_email(__input_value)[1]
- class NameEmail(_repr.Representation):
- """
- Info:
- To use this type, you need to install the optional
- [`email-validator`](https://github.com/JoshData/python-email-validator) package:
- ```bash
- pip install email-validator
- ```
- Validate a name and email address combination, as specified by
- [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322#section-3.4).
- The `NameEmail` has two properties: `name` and `email`.
- In case the `name` is not provided, it's inferred from the email address.
- ```py
- from pydantic import BaseModel, NameEmail
- class User(BaseModel):
- email: NameEmail
- user = User(email='Fred Bloggs <fred.bloggs@example.com>')
- print(user.email)
- #> Fred Bloggs <fred.bloggs@example.com>
- print(user.email.name)
- #> Fred Bloggs
- user = User(email='fred.bloggs@example.com')
- print(user.email)
- #> fred.bloggs <fred.bloggs@example.com>
- print(user.email.name)
- #> fred.bloggs
- ```
- """ # noqa: D212
- __slots__ = 'name', 'email'
- def __init__(self, name: str, email: str):
- self.name = name
- self.email = email
- def __eq__(self, other: Any) -> bool:
- return isinstance(other, NameEmail) and (self.name, self.email) == (other.name, other.email)
- @classmethod
- def __get_pydantic_json_schema__(
- cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler
- ) -> JsonSchemaValue:
- field_schema = handler(core_schema)
- field_schema.update(type='string', format='name-email')
- return field_schema
- @classmethod
- def __get_pydantic_core_schema__(
- cls,
- _source: type[Any],
- _handler: GetCoreSchemaHandler,
- ) -> core_schema.CoreSchema:
- import_email_validator()
- return core_schema.no_info_after_validator_function(
- cls._validate,
- core_schema.union_schema(
- [core_schema.is_instance_schema(cls), core_schema.str_schema()],
- custom_error_type='name_email_type',
- custom_error_message='Input is not a valid NameEmail',
- ),
- serialization=core_schema.to_string_ser_schema(),
- )
- @classmethod
- def _validate(cls, __input_value: NameEmail | str) -> NameEmail:
- if isinstance(__input_value, cls):
- return __input_value
- else:
- name, email = validate_email(__input_value) # type: ignore[arg-type]
- return cls(name, email)
- def __str__(self) -> str:
- return f'{self.name} <{self.email}>'
- class IPvAnyAddress:
- """Validate an IPv4 or IPv6 address.
- ```py
- from pydantic import BaseModel
- from pydantic.networks import IPvAnyAddress
- class IpModel(BaseModel):
- ip: IPvAnyAddress
- print(IpModel(ip='127.0.0.1'))
- #> ip=IPv4Address('127.0.0.1')
- try:
- IpModel(ip='http://www.example.com')
- except ValueError as e:
- print(e.errors())
- '''
- [
- {
- 'type': 'ip_any_address',
- 'loc': ('ip',),
- 'msg': 'value is not a valid IPv4 or IPv6 address',
- 'input': 'http://www.example.com',
- }
- ]
- '''
- ```
- """
- __slots__ = ()
- def __new__(cls, value: Any) -> IPv4Address | IPv6Address:
- """Validate an IPv4 or IPv6 address."""
- try:
- return IPv4Address(value)
- except ValueError:
- pass
- try:
- return IPv6Address(value)
- except ValueError:
- raise PydanticCustomError('ip_any_address', 'value is not a valid IPv4 or IPv6 address')
- @classmethod
- def __get_pydantic_json_schema__(
- cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler
- ) -> JsonSchemaValue:
- field_schema = {}
- field_schema.update(type='string', format='ipvanyaddress')
- return field_schema
- @classmethod
- def __get_pydantic_core_schema__(
- cls,
- _source: type[Any],
- _handler: GetCoreSchemaHandler,
- ) -> core_schema.CoreSchema:
- return core_schema.no_info_plain_validator_function(
- cls._validate, serialization=core_schema.to_string_ser_schema()
- )
- @classmethod
- def _validate(cls, __input_value: Any) -> IPv4Address | IPv6Address:
- return cls(__input_value) # type: ignore[return-value]
- class IPvAnyInterface:
- """Validate an IPv4 or IPv6 interface."""
- __slots__ = ()
- def __new__(cls, value: NetworkType) -> IPv4Interface | IPv6Interface:
- """Validate an IPv4 or IPv6 interface."""
- try:
- return IPv4Interface(value)
- except ValueError:
- pass
- try:
- return IPv6Interface(value)
- except ValueError:
- raise PydanticCustomError('ip_any_interface', 'value is not a valid IPv4 or IPv6 interface')
- @classmethod
- def __get_pydantic_json_schema__(
- cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler
- ) -> JsonSchemaValue:
- field_schema = {}
- field_schema.update(type='string', format='ipvanyinterface')
- return field_schema
- @classmethod
- def __get_pydantic_core_schema__(
- cls,
- _source: type[Any],
- _handler: GetCoreSchemaHandler,
- ) -> core_schema.CoreSchema:
- return core_schema.no_info_plain_validator_function(
- cls._validate, serialization=core_schema.to_string_ser_schema()
- )
- @classmethod
- def _validate(cls, __input_value: NetworkType) -> IPv4Interface | IPv6Interface:
- return cls(__input_value) # type: ignore[return-value]
- class IPvAnyNetwork:
- """Validate an IPv4 or IPv6 network."""
- __slots__ = ()
- def __new__(cls, value: NetworkType) -> IPv4Network | IPv6Network:
- """Validate an IPv4 or IPv6 network."""
- # Assume IP Network is defined with a default value for `strict` argument.
- # Define your own class if you want to specify network address check strictness.
- try:
- return IPv4Network(value)
- except ValueError:
- pass
- try:
- return IPv6Network(value)
- except ValueError:
- raise PydanticCustomError('ip_any_network', 'value is not a valid IPv4 or IPv6 network')
- @classmethod
- def __get_pydantic_json_schema__(
- cls, core_schema: core_schema.CoreSchema, handler: _schema_generation_shared.GetJsonSchemaHandler
- ) -> JsonSchemaValue:
- field_schema = {}
- field_schema.update(type='string', format='ipvanynetwork')
- return field_schema
- @classmethod
- def __get_pydantic_core_schema__(
- cls,
- _source: type[Any],
- _handler: GetCoreSchemaHandler,
- ) -> core_schema.CoreSchema:
- return core_schema.no_info_plain_validator_function(
- cls._validate, serialization=core_schema.to_string_ser_schema()
- )
- @classmethod
- def _validate(cls, __input_value: NetworkType) -> IPv4Network | IPv6Network:
- return cls(__input_value) # type: ignore[return-value]
- def _build_pretty_email_regex() -> re.Pattern[str]:
- name_chars = r'[\w!#$%&\'*+\-/=?^_`{|}~]'
- unquoted_name_group = fr'((?:{name_chars}+\s+)*{name_chars}+)'
- quoted_name_group = r'"((?:[^"]|\")+)"'
- email_group = r'<\s*(.+)\s*>'
- return re.compile(rf'\s*(?:{unquoted_name_group}|{quoted_name_group})?\s*{email_group}\s*')
- pretty_email_regex = _build_pretty_email_regex()
- MAX_EMAIL_LENGTH = 2048
- """Maximum length for an email.
- A somewhat arbitrary but very generous number compared to what is allowed by most implementations.
- """
- def validate_email(value: str) -> tuple[str, str]:
- """Email address validation using [email-validator](https://pypi.org/project/email-validator/).
- Note:
- Note that:
- * Raw IP address (literal) domain parts are not allowed.
- * `"John Doe <local_part@domain.com>"` style "pretty" email addresses are processed.
- * Spaces are striped from the beginning and end of addresses, but no error is raised.
- """
- if email_validator is None:
- import_email_validator()
- if len(value) > MAX_EMAIL_LENGTH:
- raise PydanticCustomError(
- 'value_error',
- 'value is not a valid email address: {reason}',
- {'reason': f'Length must not exceed {MAX_EMAIL_LENGTH} characters'},
- )
- m = pretty_email_regex.fullmatch(value)
- name: str | None = None
- if m:
- unquoted_name, quoted_name, value = m.groups()
- name = unquoted_name or quoted_name
- email = value.strip()
- try:
- parts = email_validator.validate_email(email, check_deliverability=False)
- except email_validator.EmailNotValidError as e:
- raise PydanticCustomError(
- 'value_error', 'value is not a valid email address: {reason}', {'reason': str(e.args[0])}
- ) from e
- email = parts.normalized
- assert email is not None
- name = name or parts.local_part
- return name, email
- __getattr__ = getattr_migration(__name__)
|