Source code for outgoing.config

from collections.abc import Mapping
import pathlib
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Union
import pydantic
from . import core
from .errors import InvalidPasswordError
from .util import AnyPath, resolve_path

if TYPE_CHECKING:
    from pydantic.typing import CallableGenerator

    Path = pathlib.Path
    FilePath = pathlib.Path
    DirectoryPath = pathlib.Path

else:

[docs] class Path(pathlib.Path): """ Converts its input to `pathlib.Path` instances, including expanding tildes. If there is a field named ``configpath`` declared before the `Path` field and its value is non-`None`, then the value of the `Path` field will be resolved relative to the parent directory of the ``configpath`` field; otherwise, it will be resolved relative to the current directory. """ @classmethod def __get_validators__(cls) -> "CallableGenerator": yield path_resolve
[docs] class FilePath(pydantic.FilePath): """ Like `Path`, but the path must exist and be a file """ @classmethod def __get_validators__(cls) -> "CallableGenerator": yield path_resolve yield from super().__get_validators__()
[docs] class DirectoryPath(pydantic.DirectoryPath): """ Like `Path`, but the path must exist and be a directory """ @classmethod def __get_validators__(cls) -> "CallableGenerator": yield path_resolve yield from super().__get_validators__()
# We have to implement Password configuration via explicit subclassing as using # a function instead à la pydantic's conlist() leads to mypy errors; cf. # <https://github.com/samuelcolvin/pydantic/issues/975>
[docs]class Password(pydantic.SecretStr): """ A subclass of `pydantic.SecretStr` that accepts ``outgoing`` password specifiers as input and automatically resolves them using `resolve_password()`. Host, username, and ``configpath`` values are passed to `resolve_password()` as follows: - If `Password` is subclassed and given a ``host`` class variable naming a field, and if the subclass is then used in a model where a field with that name is declared before the `Password` subclass field, then when the model is instantiated, the value of the named field will be passed as the ``host`` argument to `resolve_password()`. (If the named field is not present on the model that uses the subclass, the `Password` field will fail validation.) - Alternatively, `Password` can be subclassed with ``host`` set to a class callable (a classmethod or staticmethod), and when that subclass is used in a model being instantiated, the callable will be passed a `dict` of all validated fields declared before the password field; the return value from the callable will then be passed as the ``host`` argument to `resolve_password()`. (If the callable raises an exception, the `Password` field will fail validation.) - If `Password` is used in a model without being subclassed, or if ``host`` is not defined in a subclass, then `None` will be passed as the ``host`` argument to `resolve_password()`. - The ``username`` argument to `resolve_password()` can likewise be defined by subclassing `Password` and defining ``username`` appropriately. - If there is a field named ``configpath`` declared before the `Password` field, then the value of ``configpath`` is passed to `resolve_password()`. For example, if writing a pydantic model for a sender configuration where the host-analogue value is passed in a field named ``"service"`` and for which the username is always ``"__token__"``, you would subclass `Password` like this: .. code:: python class MyPassword(outgoing.Password): host = "service" @staticmethod def username(values: Dict[str, Any]) -> str: return "__token__" and then use it in your model like so: .. code:: python class MySender(pydantic.BaseModel): configpath: Optional[outgoing.Path] service: str password: MyPassword # Must come after `configpath` and `service`! # ... other fields ... Then, when ``MySender`` is instantiated, the input to the ``password`` field would be automatically resolved by doing (effectively): .. code:: python my_sender.password = pydantic.SecretStr( resolve_password( my_sender.password, host=my_sender.service, username="__token__", configpath=my_sender.configpath, ) ) .. note:: As this is a subclass of `pydantic.SecretStr`, the value of a `Password` field is retrieved by calling its ``get_secret_value()`` method. """ host: ClassVar[Any] = None username: ClassVar[Any] = None def __init_subclass__(cls) -> None: if ( cls.host is not None and not isinstance(cls.host, str) and not callable(cls.host) ): raise RuntimeError("Password.host must be a str, callable, or None") if ( cls.username is not None and not isinstance(cls.username, str) and not callable(cls.username) ): raise RuntimeError("Password.username must be a str, callable, or None") @classmethod def __get_validators__(cls) -> "CallableGenerator": yield cls._resolve yield from super().__get_validators__() @classmethod def _resolve(cls, v: Any, values: Dict[str, Any]) -> str: if not isinstance(v, (str, Mapping)): raise ValueError( "Password must be either a string or an object with exactly one field" ) if isinstance(cls.host, str): try: host = values[cls.host] except KeyError: raise ValueError("Insufficient data to determine password") elif callable(cls.host): try: host = cls.host(values) except Exception: raise ValueError("Insufficient data to determine password") else: assert cls.host is None host = None if isinstance(cls.username, str): try: username = values[cls.username] except KeyError: raise ValueError("Insufficient data to determine password") elif callable(cls.username): try: username = cls.username(values) except Exception: raise ValueError("Insufficient data to determine password") else: assert cls.username is None username = None try: return core.resolve_password( v, host=host, username=username, configpath=values.get("configpath"), ) except InvalidPasswordError as e: raise ValueError(e.details)
def path_resolve(v: AnyPath, values: Dict[str, Any]) -> pathlib.Path: return resolve_path(v, values.get("configpath"))
[docs]class StandardPassword(Password): """ A subclass of `Password` in which ``host`` is set to ``"host"`` and ``username`` is set to ``"username"`` """ host = "host" username = "username"
[docs]class NetrcConfig(pydantic.BaseModel): """ A pydantic model usable as a base class for any senders that wish to support both ``password`` fields and netrc files. The model accepts the fields ``configpath``, ``netrc`` (a boolean or a file path; defaults to `False`), ``host`` (required), ``username`` (optional), and ``password`` (optional). When the model is instantiated, if ``password`` is `None` but ``netrc`` is true or a filepath, the entry for ``host`` is looked up in :file:`~/.netrc` or the given file, and the ``username`` and ``password`` fields are set to the values found. The model will raise a validation error if any of the following are true: - ``password`` is set but ``netrc`` is true - ``password`` is set but ``username`` is not set - ``username`` is set but ``password`` is not set and ``netrc`` is false - ``netrc`` is true or a filepath, ``username`` is non-`None`, and the username in the netrc file differs from ``username`` - ``netrc`` is true or a filepath and no entry can be found in the netrc file """ configpath: Optional[Path] netrc: Union[pydantic.StrictBool, FilePath] = False host: str username: Optional[str] password: Optional[StandardPassword] @pydantic.root_validator(skip_on_failure=True) def _validate(cls, values: Dict[str, Any]) -> Dict[str, Any]: # noqa: B902, U100 if values["password"] is not None: if values["netrc"]: raise ValueError("netrc cannot be set when a password is present") elif values["username"] is None: raise ValueError("Password cannot be given without username") elif values["netrc"]: path: Optional[Path] if isinstance(values["netrc"], bool): path = None else: path = values["netrc"] try: username, password = core.lookup_netrc( values["host"], username=values["username"], path=path ) except Exception as e: raise ValueError(f"Error retrieving password from netrc file: {e}") values["username"] = username values["password"] = StandardPassword(password) elif values["username"] is not None: raise ValueError("Username cannot be given without netrc or password") return values