Загрузить файлы в «venv/Lib/site-packages/pydantic/v1»
This commit is contained in:
191
venv/Lib/site-packages/pydantic/v1/config.py
Normal file
191
venv/Lib/site-packages/pydantic/v1/config.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import json
|
||||||
|
from enum import Enum
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Dict, ForwardRef, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
|
from typing_extensions import Literal, Protocol
|
||||||
|
|
||||||
|
from pydantic.v1.typing import AnyArgTCallable, AnyCallable
|
||||||
|
from pydantic.v1.utils import GetterDict
|
||||||
|
from pydantic.v1.version import compiled
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
from pydantic.v1.fields import ModelField
|
||||||
|
from pydantic.v1.main import BaseModel
|
||||||
|
|
||||||
|
ConfigType = Type['BaseConfig']
|
||||||
|
|
||||||
|
class SchemaExtraCallable(Protocol):
|
||||||
|
@overload
|
||||||
|
def __call__(self, schema: Dict[str, Any]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __call__(self, schema: Dict[str, Any], model_class: Type[BaseModel]) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
SchemaExtraCallable = Callable[..., None]
|
||||||
|
|
||||||
|
__all__ = 'BaseConfig', 'ConfigDict', 'get_config', 'Extra', 'inherit_config', 'prepare_config'
|
||||||
|
|
||||||
|
|
||||||
|
class Extra(str, Enum):
|
||||||
|
allow = 'allow'
|
||||||
|
ignore = 'ignore'
|
||||||
|
forbid = 'forbid'
|
||||||
|
|
||||||
|
|
||||||
|
# https://github.com/cython/cython/issues/4003
|
||||||
|
# Fixed in Cython 3 and Pydantic v1 won't support Cython 3.
|
||||||
|
# Pydantic v2 doesn't depend on Cython at all.
|
||||||
|
if not compiled:
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
|
class ConfigDict(TypedDict, total=False):
|
||||||
|
title: Optional[str]
|
||||||
|
anystr_lower: bool
|
||||||
|
anystr_strip_whitespace: bool
|
||||||
|
min_anystr_length: int
|
||||||
|
max_anystr_length: Optional[int]
|
||||||
|
validate_all: bool
|
||||||
|
extra: Extra
|
||||||
|
allow_mutation: bool
|
||||||
|
frozen: bool
|
||||||
|
allow_population_by_field_name: bool
|
||||||
|
use_enum_values: bool
|
||||||
|
fields: Dict[str, Union[str, Dict[str, str]]]
|
||||||
|
validate_assignment: bool
|
||||||
|
error_msg_templates: Dict[str, str]
|
||||||
|
arbitrary_types_allowed: bool
|
||||||
|
orm_mode: bool
|
||||||
|
getter_dict: Type[GetterDict]
|
||||||
|
alias_generator: Optional[Callable[[str], str]]
|
||||||
|
keep_untouched: Tuple[type, ...]
|
||||||
|
schema_extra: Union[Dict[str, object], 'SchemaExtraCallable']
|
||||||
|
json_loads: Callable[[str], object]
|
||||||
|
json_dumps: AnyArgTCallable[str]
|
||||||
|
json_encoders: Dict[Type[object], AnyCallable]
|
||||||
|
underscore_attrs_are_private: bool
|
||||||
|
allow_inf_nan: bool
|
||||||
|
copy_on_model_validation: Literal['none', 'deep', 'shallow']
|
||||||
|
# whether dataclass `__post_init__` should be run after validation
|
||||||
|
post_init_call: Literal['before_validation', 'after_validation']
|
||||||
|
|
||||||
|
else:
|
||||||
|
ConfigDict = dict # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class BaseConfig:
|
||||||
|
title: Optional[str] = None
|
||||||
|
anystr_lower: bool = False
|
||||||
|
anystr_upper: bool = False
|
||||||
|
anystr_strip_whitespace: bool = False
|
||||||
|
min_anystr_length: int = 0
|
||||||
|
max_anystr_length: Optional[int] = None
|
||||||
|
validate_all: bool = False
|
||||||
|
extra: Extra = Extra.ignore
|
||||||
|
allow_mutation: bool = True
|
||||||
|
frozen: bool = False
|
||||||
|
allow_population_by_field_name: bool = False
|
||||||
|
use_enum_values: bool = False
|
||||||
|
fields: Dict[str, Union[str, Dict[str, str]]] = {}
|
||||||
|
validate_assignment: bool = False
|
||||||
|
error_msg_templates: Dict[str, str] = {}
|
||||||
|
arbitrary_types_allowed: bool = False
|
||||||
|
orm_mode: bool = False
|
||||||
|
getter_dict: Type[GetterDict] = GetterDict
|
||||||
|
alias_generator: Optional[Callable[[str], str]] = None
|
||||||
|
keep_untouched: Tuple[type, ...] = ()
|
||||||
|
schema_extra: Union[Dict[str, Any], 'SchemaExtraCallable'] = {}
|
||||||
|
json_loads: Callable[[str], Any] = json.loads
|
||||||
|
json_dumps: Callable[..., str] = json.dumps
|
||||||
|
json_encoders: Dict[Union[Type[Any], str, ForwardRef], AnyCallable] = {}
|
||||||
|
underscore_attrs_are_private: bool = False
|
||||||
|
allow_inf_nan: bool = True
|
||||||
|
|
||||||
|
# whether inherited models as fields should be reconstructed as base model,
|
||||||
|
# and whether such a copy should be shallow or deep
|
||||||
|
copy_on_model_validation: Literal['none', 'deep', 'shallow'] = 'shallow'
|
||||||
|
|
||||||
|
# whether `Union` should check all allowed types before even trying to coerce
|
||||||
|
smart_union: bool = False
|
||||||
|
# whether dataclass `__post_init__` should be run before or after validation
|
||||||
|
post_init_call: Literal['before_validation', 'after_validation'] = 'before_validation'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_field_info(cls, name: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get properties of FieldInfo from the `fields` property of the config class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
fields_value = cls.fields.get(name)
|
||||||
|
|
||||||
|
if isinstance(fields_value, str):
|
||||||
|
field_info: Dict[str, Any] = {'alias': fields_value}
|
||||||
|
elif isinstance(fields_value, dict):
|
||||||
|
field_info = fields_value
|
||||||
|
else:
|
||||||
|
field_info = {}
|
||||||
|
|
||||||
|
if 'alias' in field_info:
|
||||||
|
field_info.setdefault('alias_priority', 2)
|
||||||
|
|
||||||
|
if field_info.get('alias_priority', 0) <= 1 and cls.alias_generator:
|
||||||
|
alias = cls.alias_generator(name)
|
||||||
|
if not isinstance(alias, str):
|
||||||
|
raise TypeError(f'Config.alias_generator must return str, not {alias.__class__}')
|
||||||
|
field_info.update(alias=alias, alias_priority=1)
|
||||||
|
return field_info
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def prepare_field(cls, field: 'ModelField') -> None:
|
||||||
|
"""
|
||||||
|
Optional hook to check or modify fields during model creation.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(config: Union[ConfigDict, Type[object], None]) -> Type[BaseConfig]:
|
||||||
|
if config is None:
|
||||||
|
return BaseConfig
|
||||||
|
|
||||||
|
else:
|
||||||
|
config_dict = (
|
||||||
|
config
|
||||||
|
if isinstance(config, dict)
|
||||||
|
else {k: getattr(config, k) for k in dir(config) if not k.startswith('__')}
|
||||||
|
)
|
||||||
|
|
||||||
|
class Config(BaseConfig):
|
||||||
|
...
|
||||||
|
|
||||||
|
for k, v in config_dict.items():
|
||||||
|
setattr(Config, k, v)
|
||||||
|
return Config
|
||||||
|
|
||||||
|
|
||||||
|
def inherit_config(self_config: 'ConfigType', parent_config: 'ConfigType', **namespace: Any) -> 'ConfigType':
|
||||||
|
if not self_config:
|
||||||
|
base_classes: Tuple['ConfigType', ...] = (parent_config,)
|
||||||
|
elif self_config == parent_config:
|
||||||
|
base_classes = (self_config,)
|
||||||
|
else:
|
||||||
|
base_classes = self_config, parent_config
|
||||||
|
|
||||||
|
namespace['json_encoders'] = {
|
||||||
|
**getattr(parent_config, 'json_encoders', {}),
|
||||||
|
**getattr(self_config, 'json_encoders', {}),
|
||||||
|
**namespace.get('json_encoders', {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return type('Config', base_classes, namespace)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_config(config: Type[BaseConfig], cls_name: str) -> None:
|
||||||
|
if not isinstance(config.extra, Extra):
|
||||||
|
try:
|
||||||
|
config.extra = Extra(config.extra)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f'"{cls_name}": {config.extra} is not a valid value for "extra"')
|
||||||
500
venv/Lib/site-packages/pydantic/v1/dataclasses.py
Normal file
500
venv/Lib/site-packages/pydantic/v1/dataclasses.py
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
"""
|
||||||
|
The main purpose is to enhance stdlib dataclasses by adding validation
|
||||||
|
A pydantic dataclass can be generated from scratch or from a stdlib one.
|
||||||
|
|
||||||
|
Behind the scene, a pydantic dataclass is just like a regular one on which we attach
|
||||||
|
a `BaseModel` and magic methods to trigger the validation of the data.
|
||||||
|
`__init__` and `__post_init__` are hence overridden and have extra logic to be
|
||||||
|
able to validate input data.
|
||||||
|
|
||||||
|
When a pydantic dataclass is generated from scratch, it's just a plain dataclass
|
||||||
|
with validation triggered at initialization
|
||||||
|
|
||||||
|
The tricky part if for stdlib dataclasses that are converted after into pydantic ones e.g.
|
||||||
|
|
||||||
|
```py
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class M:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
ValidatedM = pydantic.dataclasses.dataclass(M)
|
||||||
|
```
|
||||||
|
|
||||||
|
We indeed still want to support equality, hashing, repr, ... as if it was the stdlib one!
|
||||||
|
|
||||||
|
```py
|
||||||
|
assert isinstance(ValidatedM(x=1), M)
|
||||||
|
assert ValidatedM(x=1) == M(x=1)
|
||||||
|
```
|
||||||
|
|
||||||
|
This means we **don't want to create a new dataclass that inherits from it**
|
||||||
|
The trick is to create a wrapper around `M` that will act as a proxy to trigger
|
||||||
|
validation without altering default `M` behaviour.
|
||||||
|
"""
|
||||||
|
import copy
|
||||||
|
import dataclasses
|
||||||
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
try:
|
||||||
|
from functools import cached_property
|
||||||
|
except ImportError:
|
||||||
|
# cached_property available only for python3.8+
|
||||||
|
pass
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Generator, Optional, Type, TypeVar, Union, overload
|
||||||
|
|
||||||
|
from typing_extensions import dataclass_transform
|
||||||
|
|
||||||
|
from pydantic.v1.class_validators import gather_all_validators
|
||||||
|
from pydantic.v1.config import BaseConfig, ConfigDict, Extra, get_config
|
||||||
|
from pydantic.v1.error_wrappers import ValidationError
|
||||||
|
from pydantic.v1.errors import DataclassTypeError
|
||||||
|
from pydantic.v1.fields import Field, FieldInfo, Required, Undefined
|
||||||
|
from pydantic.v1.main import create_model, validate_model
|
||||||
|
from pydantic.v1.utils import ClassAttribute
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pydantic.v1.main import BaseModel
|
||||||
|
from pydantic.v1.typing import CallableGenerator, NoArgAnyCallable
|
||||||
|
|
||||||
|
DataclassT = TypeVar('DataclassT', bound='Dataclass')
|
||||||
|
|
||||||
|
DataclassClassOrWrapper = Union[Type['Dataclass'], 'DataclassProxy']
|
||||||
|
|
||||||
|
class Dataclass:
|
||||||
|
# stdlib attributes
|
||||||
|
__dataclass_fields__: ClassVar[Dict[str, Any]]
|
||||||
|
__dataclass_params__: ClassVar[Any] # in reality `dataclasses._DataclassParams`
|
||||||
|
__post_init__: ClassVar[Callable[..., None]]
|
||||||
|
|
||||||
|
# Added by pydantic
|
||||||
|
__pydantic_run_validation__: ClassVar[bool]
|
||||||
|
__post_init_post_parse__: ClassVar[Callable[..., None]]
|
||||||
|
__pydantic_initialised__: ClassVar[bool]
|
||||||
|
__pydantic_model__: ClassVar[Type[BaseModel]]
|
||||||
|
__pydantic_validate_values__: ClassVar[Callable[['Dataclass'], None]]
|
||||||
|
__pydantic_has_field_info_default__: ClassVar[bool] # whether a `pydantic.Field` is used as default value
|
||||||
|
|
||||||
|
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __get_validators__(cls: Type['Dataclass']) -> 'CallableGenerator':
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __validate__(cls: Type['DataclassT'], v: Any) -> 'DataclassT':
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'dataclass',
|
||||||
|
'set_validation',
|
||||||
|
'create_pydantic_model_from_dataclass',
|
||||||
|
'is_builtin_dataclass',
|
||||||
|
'make_dataclass_validator',
|
||||||
|
]
|
||||||
|
|
||||||
|
_T = TypeVar('_T')
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
|
||||||
|
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
|
||||||
|
@overload
|
||||||
|
def dataclass(
|
||||||
|
*,
|
||||||
|
init: bool = True,
|
||||||
|
repr: bool = True,
|
||||||
|
eq: bool = True,
|
||||||
|
order: bool = False,
|
||||||
|
unsafe_hash: bool = False,
|
||||||
|
frozen: bool = False,
|
||||||
|
config: Union[ConfigDict, Type[object], None] = None,
|
||||||
|
validate_on_init: Optional[bool] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
|
kw_only: bool = ...,
|
||||||
|
) -> Callable[[Type[_T]], 'DataclassClassOrWrapper']:
|
||||||
|
...
|
||||||
|
|
||||||
|
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
|
||||||
|
@overload
|
||||||
|
def dataclass(
|
||||||
|
_cls: Type[_T],
|
||||||
|
*,
|
||||||
|
init: bool = True,
|
||||||
|
repr: bool = True,
|
||||||
|
eq: bool = True,
|
||||||
|
order: bool = False,
|
||||||
|
unsafe_hash: bool = False,
|
||||||
|
frozen: bool = False,
|
||||||
|
config: Union[ConfigDict, Type[object], None] = None,
|
||||||
|
validate_on_init: Optional[bool] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
|
kw_only: bool = ...,
|
||||||
|
) -> 'DataclassClassOrWrapper':
|
||||||
|
...
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
|
||||||
|
@overload
|
||||||
|
def dataclass(
|
||||||
|
*,
|
||||||
|
init: bool = True,
|
||||||
|
repr: bool = True,
|
||||||
|
eq: bool = True,
|
||||||
|
order: bool = False,
|
||||||
|
unsafe_hash: bool = False,
|
||||||
|
frozen: bool = False,
|
||||||
|
config: Union[ConfigDict, Type[object], None] = None,
|
||||||
|
validate_on_init: Optional[bool] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
|
) -> Callable[[Type[_T]], 'DataclassClassOrWrapper']:
|
||||||
|
...
|
||||||
|
|
||||||
|
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
|
||||||
|
@overload
|
||||||
|
def dataclass(
|
||||||
|
_cls: Type[_T],
|
||||||
|
*,
|
||||||
|
init: bool = True,
|
||||||
|
repr: bool = True,
|
||||||
|
eq: bool = True,
|
||||||
|
order: bool = False,
|
||||||
|
unsafe_hash: bool = False,
|
||||||
|
frozen: bool = False,
|
||||||
|
config: Union[ConfigDict, Type[object], None] = None,
|
||||||
|
validate_on_init: Optional[bool] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
|
) -> 'DataclassClassOrWrapper':
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass_transform(field_specifiers=(dataclasses.field, Field))
|
||||||
|
def dataclass(
|
||||||
|
_cls: Optional[Type[_T]] = None,
|
||||||
|
*,
|
||||||
|
init: bool = True,
|
||||||
|
repr: bool = True,
|
||||||
|
eq: bool = True,
|
||||||
|
order: bool = False,
|
||||||
|
unsafe_hash: bool = False,
|
||||||
|
frozen: bool = False,
|
||||||
|
config: Union[ConfigDict, Type[object], None] = None,
|
||||||
|
validate_on_init: Optional[bool] = None,
|
||||||
|
use_proxy: Optional[bool] = None,
|
||||||
|
kw_only: bool = False,
|
||||||
|
) -> Union[Callable[[Type[_T]], 'DataclassClassOrWrapper'], 'DataclassClassOrWrapper']:
|
||||||
|
"""
|
||||||
|
Like the python standard lib dataclasses but with type validation.
|
||||||
|
The result is either a pydantic dataclass that will validate input data
|
||||||
|
or a wrapper that will trigger validation around a stdlib dataclass
|
||||||
|
to avoid modifying it directly
|
||||||
|
"""
|
||||||
|
the_config = get_config(config)
|
||||||
|
|
||||||
|
def wrap(cls: Type[Any]) -> 'DataclassClassOrWrapper':
|
||||||
|
should_use_proxy = (
|
||||||
|
use_proxy
|
||||||
|
if use_proxy is not None
|
||||||
|
else (
|
||||||
|
is_builtin_dataclass(cls)
|
||||||
|
and (cls.__bases__[0] is object or set(dir(cls)) == set(dir(cls.__bases__[0])))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if should_use_proxy:
|
||||||
|
dc_cls_doc = ''
|
||||||
|
dc_cls = DataclassProxy(cls)
|
||||||
|
default_validate_on_init = False
|
||||||
|
else:
|
||||||
|
dc_cls_doc = cls.__doc__ or '' # needs to be done before generating dataclass
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
dc_cls = dataclasses.dataclass(
|
||||||
|
cls,
|
||||||
|
init=init,
|
||||||
|
repr=repr,
|
||||||
|
eq=eq,
|
||||||
|
order=order,
|
||||||
|
unsafe_hash=unsafe_hash,
|
||||||
|
frozen=frozen,
|
||||||
|
kw_only=kw_only,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
dc_cls = dataclasses.dataclass( # type: ignore
|
||||||
|
cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen
|
||||||
|
)
|
||||||
|
default_validate_on_init = True
|
||||||
|
|
||||||
|
should_validate_on_init = default_validate_on_init if validate_on_init is None else validate_on_init
|
||||||
|
_add_pydantic_validation_attributes(cls, the_config, should_validate_on_init, dc_cls_doc)
|
||||||
|
dc_cls.__pydantic_model__.__try_update_forward_refs__(**{cls.__name__: cls})
|
||||||
|
return dc_cls
|
||||||
|
|
||||||
|
if _cls is None:
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
return wrap(_cls)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def set_validation(cls: Type['DataclassT'], value: bool) -> Generator[Type['DataclassT'], None, None]:
|
||||||
|
original_run_validation = cls.__pydantic_run_validation__
|
||||||
|
try:
|
||||||
|
cls.__pydantic_run_validation__ = value
|
||||||
|
yield cls
|
||||||
|
finally:
|
||||||
|
cls.__pydantic_run_validation__ = original_run_validation
|
||||||
|
|
||||||
|
|
||||||
|
class DataclassProxy:
|
||||||
|
__slots__ = '__dataclass__'
|
||||||
|
|
||||||
|
def __init__(self, dc_cls: Type['Dataclass']) -> None:
|
||||||
|
object.__setattr__(self, '__dataclass__', dc_cls)
|
||||||
|
|
||||||
|
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
||||||
|
with set_validation(self.__dataclass__, True):
|
||||||
|
return self.__dataclass__(*args, **kwargs)
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> Any:
|
||||||
|
return getattr(self.__dataclass__, name)
|
||||||
|
|
||||||
|
def __setattr__(self, __name: str, __value: Any) -> None:
|
||||||
|
return setattr(self.__dataclass__, __name, __value)
|
||||||
|
|
||||||
|
def __instancecheck__(self, instance: Any) -> bool:
|
||||||
|
return isinstance(instance, self.__dataclass__)
|
||||||
|
|
||||||
|
def __copy__(self) -> 'DataclassProxy':
|
||||||
|
return DataclassProxy(copy.copy(self.__dataclass__))
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo: Any) -> 'DataclassProxy':
|
||||||
|
return DataclassProxy(copy.deepcopy(self.__dataclass__, memo))
|
||||||
|
|
||||||
|
|
||||||
|
def _add_pydantic_validation_attributes( # noqa: C901 (ignore complexity)
|
||||||
|
dc_cls: Type['Dataclass'],
|
||||||
|
config: Type[BaseConfig],
|
||||||
|
validate_on_init: bool,
|
||||||
|
dc_cls_doc: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
We need to replace the right method. If no `__post_init__` has been set in the stdlib dataclass
|
||||||
|
it won't even exist (code is generated on the fly by `dataclasses`)
|
||||||
|
By default, we run validation after `__init__` or `__post_init__` if defined
|
||||||
|
"""
|
||||||
|
init = dc_cls.__init__
|
||||||
|
|
||||||
|
@wraps(init)
|
||||||
|
def handle_extra_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None:
|
||||||
|
if config.extra == Extra.ignore:
|
||||||
|
init(self, *args, **{k: v for k, v in kwargs.items() if k in self.__dataclass_fields__})
|
||||||
|
|
||||||
|
elif config.extra == Extra.allow:
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
self.__dict__.setdefault(k, v)
|
||||||
|
init(self, *args, **{k: v for k, v in kwargs.items() if k in self.__dataclass_fields__})
|
||||||
|
|
||||||
|
else:
|
||||||
|
init(self, *args, **kwargs)
|
||||||
|
|
||||||
|
if hasattr(dc_cls, '__post_init__'):
|
||||||
|
try:
|
||||||
|
post_init = dc_cls.__post_init__.__wrapped__ # type: ignore[attr-defined]
|
||||||
|
except AttributeError:
|
||||||
|
post_init = dc_cls.__post_init__
|
||||||
|
|
||||||
|
@wraps(post_init)
|
||||||
|
def new_post_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None:
|
||||||
|
if config.post_init_call == 'before_validation':
|
||||||
|
post_init(self, *args, **kwargs)
|
||||||
|
|
||||||
|
if self.__class__.__pydantic_run_validation__:
|
||||||
|
self.__pydantic_validate_values__()
|
||||||
|
if hasattr(self, '__post_init_post_parse__'):
|
||||||
|
self.__post_init_post_parse__(*args, **kwargs)
|
||||||
|
|
||||||
|
if config.post_init_call == 'after_validation':
|
||||||
|
post_init(self, *args, **kwargs)
|
||||||
|
|
||||||
|
setattr(dc_cls, '__init__', handle_extra_init)
|
||||||
|
setattr(dc_cls, '__post_init__', new_post_init)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
@wraps(init)
|
||||||
|
def new_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None:
|
||||||
|
handle_extra_init(self, *args, **kwargs)
|
||||||
|
|
||||||
|
if self.__class__.__pydantic_run_validation__:
|
||||||
|
self.__pydantic_validate_values__()
|
||||||
|
|
||||||
|
if hasattr(self, '__post_init_post_parse__'):
|
||||||
|
# We need to find again the initvars. To do that we use `__dataclass_fields__` instead of
|
||||||
|
# public method `dataclasses.fields`
|
||||||
|
|
||||||
|
# get all initvars and their default values
|
||||||
|
initvars_and_values: Dict[str, Any] = {}
|
||||||
|
for i, f in enumerate(self.__class__.__dataclass_fields__.values()):
|
||||||
|
if f._field_type is dataclasses._FIELD_INITVAR: # type: ignore[attr-defined]
|
||||||
|
try:
|
||||||
|
# set arg value by default
|
||||||
|
initvars_and_values[f.name] = args[i]
|
||||||
|
except IndexError:
|
||||||
|
initvars_and_values[f.name] = kwargs.get(f.name, f.default)
|
||||||
|
|
||||||
|
self.__post_init_post_parse__(**initvars_and_values)
|
||||||
|
|
||||||
|
setattr(dc_cls, '__init__', new_init)
|
||||||
|
|
||||||
|
setattr(dc_cls, '__pydantic_run_validation__', ClassAttribute('__pydantic_run_validation__', validate_on_init))
|
||||||
|
setattr(dc_cls, '__pydantic_initialised__', False)
|
||||||
|
setattr(dc_cls, '__pydantic_model__', create_pydantic_model_from_dataclass(dc_cls, config, dc_cls_doc))
|
||||||
|
setattr(dc_cls, '__pydantic_validate_values__', _dataclass_validate_values)
|
||||||
|
setattr(dc_cls, '__validate__', classmethod(_validate_dataclass))
|
||||||
|
setattr(dc_cls, '__get_validators__', classmethod(_get_validators))
|
||||||
|
|
||||||
|
if dc_cls.__pydantic_model__.__config__.validate_assignment and not dc_cls.__dataclass_params__.frozen:
|
||||||
|
setattr(dc_cls, '__setattr__', _dataclass_validate_assignment_setattr)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_validators(cls: 'DataclassClassOrWrapper') -> 'CallableGenerator':
|
||||||
|
yield cls.__validate__
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_dataclass(cls: Type['DataclassT'], v: Any) -> 'DataclassT':
|
||||||
|
with set_validation(cls, True):
|
||||||
|
if isinstance(v, cls):
|
||||||
|
v.__pydantic_validate_values__()
|
||||||
|
return v
|
||||||
|
elif isinstance(v, (list, tuple)):
|
||||||
|
return cls(*v)
|
||||||
|
elif isinstance(v, dict):
|
||||||
|
return cls(**v)
|
||||||
|
else:
|
||||||
|
raise DataclassTypeError(class_name=cls.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_pydantic_model_from_dataclass(
|
||||||
|
dc_cls: Type['Dataclass'],
|
||||||
|
config: Type[Any] = BaseConfig,
|
||||||
|
dc_cls_doc: Optional[str] = None,
|
||||||
|
) -> Type['BaseModel']:
|
||||||
|
field_definitions: Dict[str, Any] = {}
|
||||||
|
for field in dataclasses.fields(dc_cls):
|
||||||
|
default: Any = Undefined
|
||||||
|
default_factory: Optional['NoArgAnyCallable'] = None
|
||||||
|
field_info: FieldInfo
|
||||||
|
|
||||||
|
if field.default is not dataclasses.MISSING:
|
||||||
|
default = field.default
|
||||||
|
elif field.default_factory is not dataclasses.MISSING:
|
||||||
|
default_factory = field.default_factory
|
||||||
|
else:
|
||||||
|
default = Required
|
||||||
|
|
||||||
|
if isinstance(default, FieldInfo):
|
||||||
|
field_info = default
|
||||||
|
dc_cls.__pydantic_has_field_info_default__ = True
|
||||||
|
else:
|
||||||
|
field_info = Field(default=default, default_factory=default_factory, **field.metadata)
|
||||||
|
|
||||||
|
field_definitions[field.name] = (field.type, field_info)
|
||||||
|
|
||||||
|
validators = gather_all_validators(dc_cls)
|
||||||
|
model: Type['BaseModel'] = create_model(
|
||||||
|
dc_cls.__name__,
|
||||||
|
__config__=config,
|
||||||
|
__module__=dc_cls.__module__,
|
||||||
|
__validators__=validators,
|
||||||
|
__cls_kwargs__={'__resolve_forward_refs__': False},
|
||||||
|
**field_definitions,
|
||||||
|
)
|
||||||
|
model.__doc__ = dc_cls_doc if dc_cls_doc is not None else dc_cls.__doc__ or ''
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
|
||||||
|
def _is_field_cached_property(obj: 'Dataclass', k: str) -> bool:
|
||||||
|
return isinstance(getattr(type(obj), k, None), cached_property)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def _is_field_cached_property(obj: 'Dataclass', k: str) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _dataclass_validate_values(self: 'Dataclass') -> None:
|
||||||
|
# validation errors can occur if this function is called twice on an already initialised dataclass.
|
||||||
|
# for example if Extra.forbid is enabled, it would consider __pydantic_initialised__ an invalid extra property
|
||||||
|
if getattr(self, '__pydantic_initialised__'):
|
||||||
|
return
|
||||||
|
if getattr(self, '__pydantic_has_field_info_default__', False):
|
||||||
|
# We need to remove `FieldInfo` values since they are not valid as input
|
||||||
|
# It's ok to do that because they are obviously the default values!
|
||||||
|
input_data = {
|
||||||
|
k: v
|
||||||
|
for k, v in self.__dict__.items()
|
||||||
|
if not (isinstance(v, FieldInfo) or _is_field_cached_property(self, k))
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
input_data = {k: v for k, v in self.__dict__.items() if not _is_field_cached_property(self, k)}
|
||||||
|
d, _, validation_error = validate_model(self.__pydantic_model__, input_data, cls=self.__class__)
|
||||||
|
if validation_error:
|
||||||
|
raise validation_error
|
||||||
|
self.__dict__.update(d)
|
||||||
|
object.__setattr__(self, '__pydantic_initialised__', True)
|
||||||
|
|
||||||
|
|
||||||
|
def _dataclass_validate_assignment_setattr(self: 'Dataclass', name: str, value: Any) -> None:
|
||||||
|
if self.__pydantic_initialised__:
|
||||||
|
d = dict(self.__dict__)
|
||||||
|
d.pop(name, None)
|
||||||
|
known_field = self.__pydantic_model__.__fields__.get(name, None)
|
||||||
|
if known_field:
|
||||||
|
value, error_ = known_field.validate(value, d, loc=name, cls=self.__class__)
|
||||||
|
if error_:
|
||||||
|
raise ValidationError([error_], self.__class__)
|
||||||
|
|
||||||
|
object.__setattr__(self, name, value)
|
||||||
|
|
||||||
|
|
||||||
|
def is_builtin_dataclass(_cls: Type[Any]) -> bool:
|
||||||
|
"""
|
||||||
|
Whether a class is a stdlib dataclass
|
||||||
|
(useful to discriminated a pydantic dataclass that is actually a wrapper around a stdlib dataclass)
|
||||||
|
|
||||||
|
we check that
|
||||||
|
- `_cls` is a dataclass
|
||||||
|
- `_cls` is not a processed pydantic dataclass (with a basemodel attached)
|
||||||
|
- `_cls` is not a pydantic dataclass inheriting directly from a stdlib dataclass
|
||||||
|
e.g.
|
||||||
|
```
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class A:
|
||||||
|
x: int
|
||||||
|
|
||||||
|
@pydantic.dataclasses.dataclass
|
||||||
|
class B(A):
|
||||||
|
y: int
|
||||||
|
```
|
||||||
|
In this case, when we first check `B`, we make an extra check and look at the annotations ('y'),
|
||||||
|
which won't be a superset of all the dataclass fields (only the stdlib fields i.e. 'x')
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
dataclasses.is_dataclass(_cls)
|
||||||
|
and not hasattr(_cls, '__pydantic_model__')
|
||||||
|
and set(_cls.__dataclass_fields__).issuperset(set(getattr(_cls, '__annotations__', {})))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_dataclass_validator(dc_cls: Type['Dataclass'], config: Type[BaseConfig]) -> 'CallableGenerator':
|
||||||
|
"""
|
||||||
|
Create a pydantic.dataclass from a builtin dataclass to add type validation
|
||||||
|
and yield the validators
|
||||||
|
It retrieves the parameters of the dataclass and forwards them to the newly created dataclass
|
||||||
|
"""
|
||||||
|
yield from _get_validators(dataclass(dc_cls, config=config, use_proxy=True))
|
||||||
248
venv/Lib/site-packages/pydantic/v1/datetime_parse.py
Normal file
248
venv/Lib/site-packages/pydantic/v1/datetime_parse.py
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
"""
|
||||||
|
Functions to parse datetime objects.
|
||||||
|
|
||||||
|
We're using regular expressions rather than time.strptime because:
|
||||||
|
- They provide both validation and parsing.
|
||||||
|
- They're more flexible for datetimes.
|
||||||
|
- The date/datetime/time constructors produce friendlier error messages.
|
||||||
|
|
||||||
|
Stolen from https://raw.githubusercontent.com/django/django/main/django/utils/dateparse.py at
|
||||||
|
9718fa2e8abe430c3526a9278dd976443d4ae3c6
|
||||||
|
|
||||||
|
Changed to:
|
||||||
|
* use standard python datetime types not django.utils.timezone
|
||||||
|
* raise ValueError when regex doesn't match rather than returning None
|
||||||
|
* support parsing unix timestamps for dates and datetimes
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from datetime import date, datetime, time, timedelta, timezone
|
||||||
|
from typing import Dict, Optional, Type, Union
|
||||||
|
|
||||||
|
from pydantic.v1 import errors
|
||||||
|
|
||||||
|
date_expr = r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
|
||||||
|
time_expr = (
|
||||||
|
r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
|
||||||
|
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
|
||||||
|
r'(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$'
|
||||||
|
)
|
||||||
|
|
||||||
|
date_re = re.compile(f'{date_expr}$')
|
||||||
|
time_re = re.compile(time_expr)
|
||||||
|
datetime_re = re.compile(f'{date_expr}[T ]{time_expr}')
|
||||||
|
|
||||||
|
standard_duration_re = re.compile(
|
||||||
|
r'^'
|
||||||
|
r'(?:(?P<days>-?\d+) (days?, )?)?'
|
||||||
|
r'((?:(?P<hours>-?\d+):)(?=\d+:\d+))?'
|
||||||
|
r'(?:(?P<minutes>-?\d+):)?'
|
||||||
|
r'(?P<seconds>-?\d+)'
|
||||||
|
r'(?:\.(?P<microseconds>\d{1,6})\d{0,6})?'
|
||||||
|
r'$'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Support the sections of ISO 8601 date representation that are accepted by timedelta
|
||||||
|
iso8601_duration_re = re.compile(
|
||||||
|
r'^(?P<sign>[-+]?)'
|
||||||
|
r'P'
|
||||||
|
r'(?:(?P<days>\d+(.\d+)?)D)?'
|
||||||
|
r'(?:T'
|
||||||
|
r'(?:(?P<hours>\d+(.\d+)?)H)?'
|
||||||
|
r'(?:(?P<minutes>\d+(.\d+)?)M)?'
|
||||||
|
r'(?:(?P<seconds>\d+(.\d+)?)S)?'
|
||||||
|
r')?'
|
||||||
|
r'$'
|
||||||
|
)
|
||||||
|
|
||||||
|
EPOCH = datetime(1970, 1, 1)
|
||||||
|
# if greater than this, the number is in ms, if less than or equal it's in seconds
|
||||||
|
# (in seconds this is 11th October 2603, in ms it's 20th August 1970)
|
||||||
|
MS_WATERSHED = int(2e10)
|
||||||
|
# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9
|
||||||
|
MAX_NUMBER = int(3e20)
|
||||||
|
StrBytesIntFloat = Union[str, bytes, int, float]
|
||||||
|
|
||||||
|
|
||||||
|
def get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]:
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
except TypeError:
|
||||||
|
raise TypeError(f'invalid type; expected {native_expected_type}, string, bytes, int or float')
|
||||||
|
|
||||||
|
|
||||||
|
def from_unix_seconds(seconds: Union[int, float]) -> datetime:
|
||||||
|
if seconds > MAX_NUMBER:
|
||||||
|
return datetime.max
|
||||||
|
elif seconds < -MAX_NUMBER:
|
||||||
|
return datetime.min
|
||||||
|
|
||||||
|
while abs(seconds) > MS_WATERSHED:
|
||||||
|
seconds /= 1000
|
||||||
|
dt = EPOCH + timedelta(seconds=seconds)
|
||||||
|
return dt.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_timezone(value: Optional[str], error: Type[Exception]) -> Union[None, int, timezone]:
|
||||||
|
if value == 'Z':
|
||||||
|
return timezone.utc
|
||||||
|
elif value is not None:
|
||||||
|
offset_mins = int(value[-2:]) if len(value) > 3 else 0
|
||||||
|
offset = 60 * int(value[1:3]) + offset_mins
|
||||||
|
if value[0] == '-':
|
||||||
|
offset = -offset
|
||||||
|
try:
|
||||||
|
return timezone(timedelta(minutes=offset))
|
||||||
|
except ValueError:
|
||||||
|
raise error()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_date(value: Union[date, StrBytesIntFloat]) -> date:
|
||||||
|
"""
|
||||||
|
Parse a date/int/float/string and return a datetime.date.
|
||||||
|
|
||||||
|
Raise ValueError if the input is well formatted but not a valid date.
|
||||||
|
Raise ValueError if the input isn't well formatted.
|
||||||
|
"""
|
||||||
|
if isinstance(value, date):
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value.date()
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
number = get_numeric(value, 'date')
|
||||||
|
if number is not None:
|
||||||
|
return from_unix_seconds(number).date()
|
||||||
|
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = value.decode()
|
||||||
|
|
||||||
|
match = date_re.match(value) # type: ignore
|
||||||
|
if match is None:
|
||||||
|
raise errors.DateError()
|
||||||
|
|
||||||
|
kw = {k: int(v) for k, v in match.groupdict().items()}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return date(**kw)
|
||||||
|
except ValueError:
|
||||||
|
raise errors.DateError()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time(value: Union[time, StrBytesIntFloat]) -> time:
|
||||||
|
"""
|
||||||
|
Parse a time/string and return a datetime.time.
|
||||||
|
|
||||||
|
Raise ValueError if the input is well formatted but not a valid time.
|
||||||
|
Raise ValueError if the input isn't well formatted, in particular if it contains an offset.
|
||||||
|
"""
|
||||||
|
if isinstance(value, time):
|
||||||
|
return value
|
||||||
|
|
||||||
|
number = get_numeric(value, 'time')
|
||||||
|
if number is not None:
|
||||||
|
if number >= 86400:
|
||||||
|
# doesn't make sense since the time time loop back around to 0
|
||||||
|
raise errors.TimeError()
|
||||||
|
return (datetime.min + timedelta(seconds=number)).time()
|
||||||
|
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = value.decode()
|
||||||
|
|
||||||
|
match = time_re.match(value) # type: ignore
|
||||||
|
if match is None:
|
||||||
|
raise errors.TimeError()
|
||||||
|
|
||||||
|
kw = match.groupdict()
|
||||||
|
if kw['microsecond']:
|
||||||
|
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
|
||||||
|
|
||||||
|
tzinfo = _parse_timezone(kw.pop('tzinfo'), errors.TimeError)
|
||||||
|
kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None}
|
||||||
|
kw_['tzinfo'] = tzinfo
|
||||||
|
|
||||||
|
try:
|
||||||
|
return time(**kw_) # type: ignore
|
||||||
|
except ValueError:
|
||||||
|
raise errors.TimeError()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime:
|
||||||
|
"""
|
||||||
|
Parse a datetime/int/float/string and return a datetime.datetime.
|
||||||
|
|
||||||
|
This function supports time zone offsets. When the input contains one,
|
||||||
|
the output uses a timezone with a fixed offset from UTC.
|
||||||
|
|
||||||
|
Raise ValueError if the input is well formatted but not a valid datetime.
|
||||||
|
Raise ValueError if the input isn't well formatted.
|
||||||
|
"""
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value
|
||||||
|
|
||||||
|
number = get_numeric(value, 'datetime')
|
||||||
|
if number is not None:
|
||||||
|
return from_unix_seconds(number)
|
||||||
|
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = value.decode()
|
||||||
|
|
||||||
|
match = datetime_re.match(value) # type: ignore
|
||||||
|
if match is None:
|
||||||
|
raise errors.DateTimeError()
|
||||||
|
|
||||||
|
kw = match.groupdict()
|
||||||
|
if kw['microsecond']:
|
||||||
|
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
|
||||||
|
|
||||||
|
tzinfo = _parse_timezone(kw.pop('tzinfo'), errors.DateTimeError)
|
||||||
|
kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None}
|
||||||
|
kw_['tzinfo'] = tzinfo
|
||||||
|
|
||||||
|
try:
|
||||||
|
return datetime(**kw_) # type: ignore
|
||||||
|
except ValueError:
|
||||||
|
raise errors.DateTimeError()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_duration(value: StrBytesIntFloat) -> timedelta:
|
||||||
|
"""
|
||||||
|
Parse a duration int/float/string and return a datetime.timedelta.
|
||||||
|
|
||||||
|
The preferred format for durations in Django is '%d %H:%M:%S.%f'.
|
||||||
|
|
||||||
|
Also supports ISO 8601 representation.
|
||||||
|
"""
|
||||||
|
if isinstance(value, timedelta):
|
||||||
|
return value
|
||||||
|
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
# below code requires a string
|
||||||
|
value = f'{value:f}'
|
||||||
|
elif isinstance(value, bytes):
|
||||||
|
value = value.decode()
|
||||||
|
|
||||||
|
try:
|
||||||
|
match = standard_duration_re.match(value) or iso8601_duration_re.match(value)
|
||||||
|
except TypeError:
|
||||||
|
raise TypeError('invalid type; expected timedelta, string, bytes, int or float')
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
raise errors.DurationError()
|
||||||
|
|
||||||
|
kw = match.groupdict()
|
||||||
|
sign = -1 if kw.pop('sign', '+') == '-' else 1
|
||||||
|
if kw.get('microseconds'):
|
||||||
|
kw['microseconds'] = kw['microseconds'].ljust(6, '0')
|
||||||
|
|
||||||
|
if kw.get('seconds') and kw.get('microseconds') and kw['seconds'].startswith('-'):
|
||||||
|
kw['microseconds'] = '-' + kw['microseconds']
|
||||||
|
|
||||||
|
kw_ = {k: float(v) for k, v in kw.items() if v is not None}
|
||||||
|
|
||||||
|
return sign * timedelta(**kw_)
|
||||||
264
venv/Lib/site-packages/pydantic/v1/decorator.py
Normal file
264
venv/Lib/site-packages/pydantic/v1/decorator.py
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, TypeVar, Union, overload
|
||||||
|
|
||||||
|
from pydantic.v1 import validator
|
||||||
|
from pydantic.v1.config import Extra
|
||||||
|
from pydantic.v1.errors import ConfigError
|
||||||
|
from pydantic.v1.main import BaseModel, create_model
|
||||||
|
from pydantic.v1.typing import get_all_type_hints
|
||||||
|
from pydantic.v1.utils import to_camel
|
||||||
|
|
||||||
|
__all__ = ('validate_arguments',)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pydantic.v1.typing import AnyCallable
|
||||||
|
|
||||||
|
AnyCallableT = TypeVar('AnyCallableT', bound=AnyCallable)
|
||||||
|
ConfigType = Union[None, Type[Any], Dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def validate_arguments(func: None = None, *, config: 'ConfigType' = None) -> Callable[['AnyCallableT'], 'AnyCallableT']:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def validate_arguments(func: 'AnyCallableT') -> 'AnyCallableT':
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def validate_arguments(func: Optional['AnyCallableT'] = None, *, config: 'ConfigType' = None) -> Any:
|
||||||
|
"""
|
||||||
|
Decorator to validate the arguments passed to a function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def validate(_func: 'AnyCallable') -> 'AnyCallable':
|
||||||
|
vd = ValidatedFunction(_func, config)
|
||||||
|
|
||||||
|
@wraps(_func)
|
||||||
|
def wrapper_function(*args: Any, **kwargs: Any) -> Any:
|
||||||
|
return vd.call(*args, **kwargs)
|
||||||
|
|
||||||
|
wrapper_function.vd = vd # type: ignore
|
||||||
|
wrapper_function.validate = vd.init_model_instance # type: ignore
|
||||||
|
wrapper_function.raw_function = vd.raw_function # type: ignore
|
||||||
|
wrapper_function.model = vd.model # type: ignore
|
||||||
|
return wrapper_function
|
||||||
|
|
||||||
|
if func:
|
||||||
|
return validate(func)
|
||||||
|
else:
|
||||||
|
return validate
|
||||||
|
|
||||||
|
|
||||||
|
ALT_V_ARGS = 'v__args'
|
||||||
|
ALT_V_KWARGS = 'v__kwargs'
|
||||||
|
V_POSITIONAL_ONLY_NAME = 'v__positional_only'
|
||||||
|
V_DUPLICATE_KWARGS = 'v__duplicate_kwargs'
|
||||||
|
|
||||||
|
|
||||||
|
class ValidatedFunction:
|
||||||
|
def __init__(self, function: 'AnyCallableT', config: 'ConfigType'): # noqa C901
|
||||||
|
from inspect import Parameter, signature
|
||||||
|
|
||||||
|
parameters: Mapping[str, Parameter] = signature(function).parameters
|
||||||
|
|
||||||
|
if parameters.keys() & {ALT_V_ARGS, ALT_V_KWARGS, V_POSITIONAL_ONLY_NAME, V_DUPLICATE_KWARGS}:
|
||||||
|
raise ConfigError(
|
||||||
|
f'"{ALT_V_ARGS}", "{ALT_V_KWARGS}", "{V_POSITIONAL_ONLY_NAME}" and "{V_DUPLICATE_KWARGS}" '
|
||||||
|
f'are not permitted as argument names when using the "{validate_arguments.__name__}" decorator'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.raw_function = function
|
||||||
|
self.arg_mapping: Dict[int, str] = {}
|
||||||
|
self.positional_only_args = set()
|
||||||
|
self.v_args_name = 'args'
|
||||||
|
self.v_kwargs_name = 'kwargs'
|
||||||
|
|
||||||
|
type_hints = get_all_type_hints(function)
|
||||||
|
takes_args = False
|
||||||
|
takes_kwargs = False
|
||||||
|
fields: Dict[str, Tuple[Any, Any]] = {}
|
||||||
|
for i, (name, p) in enumerate(parameters.items()):
|
||||||
|
if p.annotation is p.empty:
|
||||||
|
annotation = Any
|
||||||
|
else:
|
||||||
|
annotation = type_hints[name]
|
||||||
|
|
||||||
|
default = ... if p.default is p.empty else p.default
|
||||||
|
if p.kind == Parameter.POSITIONAL_ONLY:
|
||||||
|
self.arg_mapping[i] = name
|
||||||
|
fields[name] = annotation, default
|
||||||
|
fields[V_POSITIONAL_ONLY_NAME] = List[str], None
|
||||||
|
self.positional_only_args.add(name)
|
||||||
|
elif p.kind == Parameter.POSITIONAL_OR_KEYWORD:
|
||||||
|
self.arg_mapping[i] = name
|
||||||
|
fields[name] = annotation, default
|
||||||
|
fields[V_DUPLICATE_KWARGS] = List[str], None
|
||||||
|
elif p.kind == Parameter.KEYWORD_ONLY:
|
||||||
|
fields[name] = annotation, default
|
||||||
|
elif p.kind == Parameter.VAR_POSITIONAL:
|
||||||
|
self.v_args_name = name
|
||||||
|
fields[name] = Tuple[annotation, ...], None
|
||||||
|
takes_args = True
|
||||||
|
else:
|
||||||
|
assert p.kind == Parameter.VAR_KEYWORD, p.kind
|
||||||
|
self.v_kwargs_name = name
|
||||||
|
fields[name] = Dict[str, annotation], None # type: ignore
|
||||||
|
takes_kwargs = True
|
||||||
|
|
||||||
|
# these checks avoid a clash between "args" and a field with that name
|
||||||
|
if not takes_args and self.v_args_name in fields:
|
||||||
|
self.v_args_name = ALT_V_ARGS
|
||||||
|
|
||||||
|
# same with "kwargs"
|
||||||
|
if not takes_kwargs and self.v_kwargs_name in fields:
|
||||||
|
self.v_kwargs_name = ALT_V_KWARGS
|
||||||
|
|
||||||
|
if not takes_args:
|
||||||
|
# we add the field so validation below can raise the correct exception
|
||||||
|
fields[self.v_args_name] = List[Any], None
|
||||||
|
|
||||||
|
if not takes_kwargs:
|
||||||
|
# same with kwargs
|
||||||
|
fields[self.v_kwargs_name] = Dict[Any, Any], None
|
||||||
|
|
||||||
|
self.create_model(fields, takes_args, takes_kwargs, config)
|
||||||
|
|
||||||
|
def init_model_instance(self, *args: Any, **kwargs: Any) -> BaseModel:
|
||||||
|
values = self.build_values(args, kwargs)
|
||||||
|
return self.model(**values)
|
||||||
|
|
||||||
|
def call(self, *args: Any, **kwargs: Any) -> Any:
|
||||||
|
m = self.init_model_instance(*args, **kwargs)
|
||||||
|
return self.execute(m)
|
||||||
|
|
||||||
|
def build_values(self, args: Tuple[Any, ...], kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
values: Dict[str, Any] = {}
|
||||||
|
if args:
|
||||||
|
arg_iter = enumerate(args)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
i, a = next(arg_iter)
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
arg_name = self.arg_mapping.get(i)
|
||||||
|
if arg_name is not None:
|
||||||
|
values[arg_name] = a
|
||||||
|
else:
|
||||||
|
values[self.v_args_name] = [a] + [a for _, a in arg_iter]
|
||||||
|
break
|
||||||
|
|
||||||
|
var_kwargs: Dict[str, Any] = {}
|
||||||
|
wrong_positional_args = []
|
||||||
|
duplicate_kwargs = []
|
||||||
|
fields_alias = [
|
||||||
|
field.alias
|
||||||
|
for name, field in self.model.__fields__.items()
|
||||||
|
if name not in (self.v_args_name, self.v_kwargs_name)
|
||||||
|
]
|
||||||
|
non_var_fields = set(self.model.__fields__) - {self.v_args_name, self.v_kwargs_name}
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
if k in non_var_fields or k in fields_alias:
|
||||||
|
if k in self.positional_only_args:
|
||||||
|
wrong_positional_args.append(k)
|
||||||
|
if k in values:
|
||||||
|
duplicate_kwargs.append(k)
|
||||||
|
values[k] = v
|
||||||
|
else:
|
||||||
|
var_kwargs[k] = v
|
||||||
|
|
||||||
|
if var_kwargs:
|
||||||
|
values[self.v_kwargs_name] = var_kwargs
|
||||||
|
if wrong_positional_args:
|
||||||
|
values[V_POSITIONAL_ONLY_NAME] = wrong_positional_args
|
||||||
|
if duplicate_kwargs:
|
||||||
|
values[V_DUPLICATE_KWARGS] = duplicate_kwargs
|
||||||
|
return values
|
||||||
|
|
||||||
|
def execute(self, m: BaseModel) -> Any:
|
||||||
|
d = {k: v for k, v in m._iter() if k in m.__fields_set__ or m.__fields__[k].default_factory}
|
||||||
|
var_kwargs = d.pop(self.v_kwargs_name, {})
|
||||||
|
|
||||||
|
if self.v_args_name in d:
|
||||||
|
args_: List[Any] = []
|
||||||
|
in_kwargs = False
|
||||||
|
kwargs = {}
|
||||||
|
for name, value in d.items():
|
||||||
|
if in_kwargs:
|
||||||
|
kwargs[name] = value
|
||||||
|
elif name == self.v_args_name:
|
||||||
|
args_ += value
|
||||||
|
in_kwargs = True
|
||||||
|
else:
|
||||||
|
args_.append(value)
|
||||||
|
return self.raw_function(*args_, **kwargs, **var_kwargs)
|
||||||
|
elif self.positional_only_args:
|
||||||
|
args_ = []
|
||||||
|
kwargs = {}
|
||||||
|
for name, value in d.items():
|
||||||
|
if name in self.positional_only_args:
|
||||||
|
args_.append(value)
|
||||||
|
else:
|
||||||
|
kwargs[name] = value
|
||||||
|
return self.raw_function(*args_, **kwargs, **var_kwargs)
|
||||||
|
else:
|
||||||
|
return self.raw_function(**d, **var_kwargs)
|
||||||
|
|
||||||
|
def create_model(self, fields: Dict[str, Any], takes_args: bool, takes_kwargs: bool, config: 'ConfigType') -> None:
|
||||||
|
pos_args = len(self.arg_mapping)
|
||||||
|
|
||||||
|
class CustomConfig:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not TYPE_CHECKING: # pragma: no branch
|
||||||
|
if isinstance(config, dict):
|
||||||
|
CustomConfig = type('Config', (), config) # noqa: F811
|
||||||
|
elif config is not None:
|
||||||
|
CustomConfig = config # noqa: F811
|
||||||
|
|
||||||
|
if hasattr(CustomConfig, 'fields') or hasattr(CustomConfig, 'alias_generator'):
|
||||||
|
raise ConfigError(
|
||||||
|
'Setting the "fields" and "alias_generator" property on custom Config for '
|
||||||
|
'@validate_arguments is not yet supported, please remove.'
|
||||||
|
)
|
||||||
|
|
||||||
|
class DecoratorBaseModel(BaseModel):
|
||||||
|
@validator(self.v_args_name, check_fields=False, allow_reuse=True)
|
||||||
|
def check_args(cls, v: Optional[List[Any]]) -> Optional[List[Any]]:
|
||||||
|
if takes_args or v is None:
|
||||||
|
return v
|
||||||
|
|
||||||
|
raise TypeError(f'{pos_args} positional arguments expected but {pos_args + len(v)} given')
|
||||||
|
|
||||||
|
@validator(self.v_kwargs_name, check_fields=False, allow_reuse=True)
|
||||||
|
def check_kwargs(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||||
|
if takes_kwargs or v is None:
|
||||||
|
return v
|
||||||
|
|
||||||
|
plural = '' if len(v) == 1 else 's'
|
||||||
|
keys = ', '.join(map(repr, v.keys()))
|
||||||
|
raise TypeError(f'unexpected keyword argument{plural}: {keys}')
|
||||||
|
|
||||||
|
@validator(V_POSITIONAL_ONLY_NAME, check_fields=False, allow_reuse=True)
|
||||||
|
def check_positional_only(cls, v: Optional[List[str]]) -> None:
|
||||||
|
if v is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
plural = '' if len(v) == 1 else 's'
|
||||||
|
keys = ', '.join(map(repr, v))
|
||||||
|
raise TypeError(f'positional-only argument{plural} passed as keyword argument{plural}: {keys}')
|
||||||
|
|
||||||
|
@validator(V_DUPLICATE_KWARGS, check_fields=False, allow_reuse=True)
|
||||||
|
def check_duplicate_kwargs(cls, v: Optional[List[str]]) -> None:
|
||||||
|
if v is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
plural = '' if len(v) == 1 else 's'
|
||||||
|
keys = ', '.join(map(repr, v))
|
||||||
|
raise TypeError(f'multiple values for argument{plural}: {keys}')
|
||||||
|
|
||||||
|
class Config(CustomConfig):
|
||||||
|
extra = getattr(CustomConfig, 'extra', Extra.forbid)
|
||||||
|
|
||||||
|
self.model = create_model(to_camel(self.raw_function.__name__), __base__=DecoratorBaseModel, **fields)
|
||||||
350
venv/Lib/site-packages/pydantic/v1/env_settings.py
Normal file
350
venv/Lib/site-packages/pydantic/v1/env_settings.py
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
import os
|
||||||
|
import warnings
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import AbstractSet, Any, Callable, ClassVar, Dict, List, Mapping, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
|
from pydantic.v1.config import BaseConfig, Extra
|
||||||
|
from pydantic.v1.fields import ModelField
|
||||||
|
from pydantic.v1.main import BaseModel
|
||||||
|
from pydantic.v1.types import JsonWrapper
|
||||||
|
from pydantic.v1.typing import StrPath, display_as_type, get_origin, is_union
|
||||||
|
from pydantic.v1.utils import deep_update, lenient_issubclass, path_type, sequence_like
|
||||||
|
|
||||||
|
env_file_sentinel = str(object())
|
||||||
|
|
||||||
|
SettingsSourceCallable = Callable[['BaseSettings'], Dict[str, Any]]
|
||||||
|
DotenvType = Union[StrPath, List[StrPath], Tuple[StrPath, ...]]
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSettings(BaseModel):
|
||||||
|
"""
|
||||||
|
Base class for settings, allowing values to be overridden by environment variables.
|
||||||
|
|
||||||
|
This is useful in production for secrets you do not wish to save in code, it plays nicely with docker(-compose),
|
||||||
|
Heroku and any 12 factor app design.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
__pydantic_self__,
|
||||||
|
_env_file: Optional[DotenvType] = env_file_sentinel,
|
||||||
|
_env_file_encoding: Optional[str] = None,
|
||||||
|
_env_nested_delimiter: Optional[str] = None,
|
||||||
|
_secrets_dir: Optional[StrPath] = None,
|
||||||
|
**values: Any,
|
||||||
|
) -> None:
|
||||||
|
# Uses something other than `self` the first arg to allow "self" as a settable attribute
|
||||||
|
super().__init__(
|
||||||
|
**__pydantic_self__._build_values(
|
||||||
|
values,
|
||||||
|
_env_file=_env_file,
|
||||||
|
_env_file_encoding=_env_file_encoding,
|
||||||
|
_env_nested_delimiter=_env_nested_delimiter,
|
||||||
|
_secrets_dir=_secrets_dir,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_values(
|
||||||
|
self,
|
||||||
|
init_kwargs: Dict[str, Any],
|
||||||
|
_env_file: Optional[DotenvType] = None,
|
||||||
|
_env_file_encoding: Optional[str] = None,
|
||||||
|
_env_nested_delimiter: Optional[str] = None,
|
||||||
|
_secrets_dir: Optional[StrPath] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
# Configure built-in sources
|
||||||
|
init_settings = InitSettingsSource(init_kwargs=init_kwargs)
|
||||||
|
env_settings = EnvSettingsSource(
|
||||||
|
env_file=(_env_file if _env_file != env_file_sentinel else self.__config__.env_file),
|
||||||
|
env_file_encoding=(
|
||||||
|
_env_file_encoding if _env_file_encoding is not None else self.__config__.env_file_encoding
|
||||||
|
),
|
||||||
|
env_nested_delimiter=(
|
||||||
|
_env_nested_delimiter if _env_nested_delimiter is not None else self.__config__.env_nested_delimiter
|
||||||
|
),
|
||||||
|
env_prefix_len=len(self.__config__.env_prefix),
|
||||||
|
)
|
||||||
|
file_secret_settings = SecretsSettingsSource(secrets_dir=_secrets_dir or self.__config__.secrets_dir)
|
||||||
|
# Provide a hook to set built-in sources priority and add / remove sources
|
||||||
|
sources = self.__config__.customise_sources(
|
||||||
|
init_settings=init_settings, env_settings=env_settings, file_secret_settings=file_secret_settings
|
||||||
|
)
|
||||||
|
if sources:
|
||||||
|
return deep_update(*reversed([source(self) for source in sources]))
|
||||||
|
else:
|
||||||
|
# no one should mean to do this, but I think returning an empty dict is marginally preferable
|
||||||
|
# to an informative error and much better than a confusing error
|
||||||
|
return {}
|
||||||
|
|
||||||
|
class Config(BaseConfig):
|
||||||
|
env_prefix: str = ''
|
||||||
|
env_file: Optional[DotenvType] = None
|
||||||
|
env_file_encoding: Optional[str] = None
|
||||||
|
env_nested_delimiter: Optional[str] = None
|
||||||
|
secrets_dir: Optional[StrPath] = None
|
||||||
|
validate_all: bool = True
|
||||||
|
extra: Extra = Extra.forbid
|
||||||
|
arbitrary_types_allowed: bool = True
|
||||||
|
case_sensitive: bool = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def prepare_field(cls, field: ModelField) -> None:
|
||||||
|
env_names: Union[List[str], AbstractSet[str]]
|
||||||
|
field_info_from_config = cls.get_field_info(field.name)
|
||||||
|
|
||||||
|
env = field_info_from_config.get('env') or field.field_info.extra.get('env')
|
||||||
|
if env is None:
|
||||||
|
if field.has_alias:
|
||||||
|
warnings.warn(
|
||||||
|
'aliases are no longer used by BaseSettings to define which environment variables to read. '
|
||||||
|
'Instead use the "env" field setting. '
|
||||||
|
'See https://pydantic-docs.helpmanual.io/usage/settings/#environment-variable-names',
|
||||||
|
FutureWarning,
|
||||||
|
)
|
||||||
|
env_names = {cls.env_prefix + field.name}
|
||||||
|
elif isinstance(env, str):
|
||||||
|
env_names = {env}
|
||||||
|
elif isinstance(env, (set, frozenset)):
|
||||||
|
env_names = env
|
||||||
|
elif sequence_like(env):
|
||||||
|
env_names = list(env)
|
||||||
|
else:
|
||||||
|
raise TypeError(f'invalid field env: {env!r} ({display_as_type(env)}); should be string, list or set')
|
||||||
|
|
||||||
|
if not cls.case_sensitive:
|
||||||
|
env_names = env_names.__class__(n.lower() for n in env_names)
|
||||||
|
field.field_info.extra['env_names'] = env_names
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def customise_sources(
|
||||||
|
cls,
|
||||||
|
init_settings: SettingsSourceCallable,
|
||||||
|
env_settings: SettingsSourceCallable,
|
||||||
|
file_secret_settings: SettingsSourceCallable,
|
||||||
|
) -> Tuple[SettingsSourceCallable, ...]:
|
||||||
|
return init_settings, env_settings, file_secret_settings
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_env_var(cls, field_name: str, raw_val: str) -> Any:
|
||||||
|
return cls.json_loads(raw_val)
|
||||||
|
|
||||||
|
# populated by the metaclass using the Config class defined above, annotated here to help IDEs only
|
||||||
|
__config__: ClassVar[Type[Config]]
|
||||||
|
|
||||||
|
|
||||||
|
class InitSettingsSource:
|
||||||
|
__slots__ = ('init_kwargs',)
|
||||||
|
|
||||||
|
def __init__(self, init_kwargs: Dict[str, Any]):
|
||||||
|
self.init_kwargs = init_kwargs
|
||||||
|
|
||||||
|
def __call__(self, settings: BaseSettings) -> Dict[str, Any]:
|
||||||
|
return self.init_kwargs
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'InitSettingsSource(init_kwargs={self.init_kwargs!r})'
|
||||||
|
|
||||||
|
|
||||||
|
class EnvSettingsSource:
|
||||||
|
__slots__ = ('env_file', 'env_file_encoding', 'env_nested_delimiter', 'env_prefix_len')
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
env_file: Optional[DotenvType],
|
||||||
|
env_file_encoding: Optional[str],
|
||||||
|
env_nested_delimiter: Optional[str] = None,
|
||||||
|
env_prefix_len: int = 0,
|
||||||
|
):
|
||||||
|
self.env_file: Optional[DotenvType] = env_file
|
||||||
|
self.env_file_encoding: Optional[str] = env_file_encoding
|
||||||
|
self.env_nested_delimiter: Optional[str] = env_nested_delimiter
|
||||||
|
self.env_prefix_len: int = env_prefix_len
|
||||||
|
|
||||||
|
def __call__(self, settings: BaseSettings) -> Dict[str, Any]: # noqa C901
|
||||||
|
"""
|
||||||
|
Build environment variables suitable for passing to the Model.
|
||||||
|
"""
|
||||||
|
d: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
if settings.__config__.case_sensitive:
|
||||||
|
env_vars: Mapping[str, Optional[str]] = os.environ
|
||||||
|
else:
|
||||||
|
env_vars = {k.lower(): v for k, v in os.environ.items()}
|
||||||
|
|
||||||
|
dotenv_vars = self._read_env_files(settings.__config__.case_sensitive)
|
||||||
|
if dotenv_vars:
|
||||||
|
env_vars = {**dotenv_vars, **env_vars}
|
||||||
|
|
||||||
|
for field in settings.__fields__.values():
|
||||||
|
env_val: Optional[str] = None
|
||||||
|
for env_name in field.field_info.extra['env_names']:
|
||||||
|
env_val = env_vars.get(env_name)
|
||||||
|
if env_val is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
is_complex, allow_parse_failure = self.field_is_complex(field)
|
||||||
|
if is_complex:
|
||||||
|
if env_val is None:
|
||||||
|
# field is complex but no value found so far, try explode_env_vars
|
||||||
|
env_val_built = self.explode_env_vars(field, env_vars)
|
||||||
|
if env_val_built:
|
||||||
|
d[field.alias] = env_val_built
|
||||||
|
else:
|
||||||
|
# field is complex and there's a value, decode that as JSON, then add explode_env_vars
|
||||||
|
try:
|
||||||
|
env_val = settings.__config__.parse_env_var(field.name, env_val)
|
||||||
|
except ValueError as e:
|
||||||
|
if not allow_parse_failure:
|
||||||
|
raise SettingsError(f'error parsing env var "{env_name}"') from e
|
||||||
|
|
||||||
|
if isinstance(env_val, dict):
|
||||||
|
d[field.alias] = deep_update(env_val, self.explode_env_vars(field, env_vars))
|
||||||
|
else:
|
||||||
|
d[field.alias] = env_val
|
||||||
|
elif env_val is not None:
|
||||||
|
# simplest case, field is not complex, we only need to add the value if it was found
|
||||||
|
d[field.alias] = env_val
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
def _read_env_files(self, case_sensitive: bool) -> Dict[str, Optional[str]]:
|
||||||
|
env_files = self.env_file
|
||||||
|
if env_files is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if isinstance(env_files, (str, os.PathLike)):
|
||||||
|
env_files = [env_files]
|
||||||
|
|
||||||
|
dotenv_vars = {}
|
||||||
|
for env_file in env_files:
|
||||||
|
env_path = Path(env_file).expanduser()
|
||||||
|
if env_path.is_file():
|
||||||
|
dotenv_vars.update(
|
||||||
|
read_env_file(env_path, encoding=self.env_file_encoding, case_sensitive=case_sensitive)
|
||||||
|
)
|
||||||
|
|
||||||
|
return dotenv_vars
|
||||||
|
|
||||||
|
def field_is_complex(self, field: ModelField) -> Tuple[bool, bool]:
|
||||||
|
"""
|
||||||
|
Find out if a field is complex, and if so whether JSON errors should be ignored
|
||||||
|
"""
|
||||||
|
if lenient_issubclass(field.annotation, JsonWrapper):
|
||||||
|
return False, False
|
||||||
|
|
||||||
|
if field.is_complex():
|
||||||
|
allow_parse_failure = False
|
||||||
|
elif is_union(get_origin(field.type_)) and field.sub_fields and any(f.is_complex() for f in field.sub_fields):
|
||||||
|
allow_parse_failure = True
|
||||||
|
else:
|
||||||
|
return False, False
|
||||||
|
|
||||||
|
return True, allow_parse_failure
|
||||||
|
|
||||||
|
def explode_env_vars(self, field: ModelField, env_vars: Mapping[str, Optional[str]]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Process env_vars and extract the values of keys containing env_nested_delimiter into nested dictionaries.
|
||||||
|
|
||||||
|
This is applied to a single field, hence filtering by env_var prefix.
|
||||||
|
"""
|
||||||
|
prefixes = [f'{env_name}{self.env_nested_delimiter}' for env_name in field.field_info.extra['env_names']]
|
||||||
|
result: Dict[str, Any] = {}
|
||||||
|
for env_name, env_val in env_vars.items():
|
||||||
|
if not any(env_name.startswith(prefix) for prefix in prefixes):
|
||||||
|
continue
|
||||||
|
# we remove the prefix before splitting in case the prefix has characters in common with the delimiter
|
||||||
|
env_name_without_prefix = env_name[self.env_prefix_len :]
|
||||||
|
_, *keys, last_key = env_name_without_prefix.split(self.env_nested_delimiter)
|
||||||
|
env_var = result
|
||||||
|
for key in keys:
|
||||||
|
env_var = env_var.setdefault(key, {})
|
||||||
|
env_var[last_key] = env_val
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f'EnvSettingsSource(env_file={self.env_file!r}, env_file_encoding={self.env_file_encoding!r}, '
|
||||||
|
f'env_nested_delimiter={self.env_nested_delimiter!r})'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SecretsSettingsSource:
|
||||||
|
__slots__ = ('secrets_dir',)
|
||||||
|
|
||||||
|
def __init__(self, secrets_dir: Optional[StrPath]):
|
||||||
|
self.secrets_dir: Optional[StrPath] = secrets_dir
|
||||||
|
|
||||||
|
def __call__(self, settings: BaseSettings) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Build fields from "secrets" files.
|
||||||
|
"""
|
||||||
|
secrets: Dict[str, Optional[str]] = {}
|
||||||
|
|
||||||
|
if self.secrets_dir is None:
|
||||||
|
return secrets
|
||||||
|
|
||||||
|
secrets_path = Path(self.secrets_dir).expanduser()
|
||||||
|
|
||||||
|
if not secrets_path.exists():
|
||||||
|
warnings.warn(f'directory "{secrets_path}" does not exist')
|
||||||
|
return secrets
|
||||||
|
|
||||||
|
if not secrets_path.is_dir():
|
||||||
|
raise SettingsError(f'secrets_dir must reference a directory, not a {path_type(secrets_path)}')
|
||||||
|
|
||||||
|
for field in settings.__fields__.values():
|
||||||
|
for env_name in field.field_info.extra['env_names']:
|
||||||
|
path = find_case_path(secrets_path, env_name, settings.__config__.case_sensitive)
|
||||||
|
if not path:
|
||||||
|
# path does not exist, we currently don't return a warning for this
|
||||||
|
continue
|
||||||
|
|
||||||
|
if path.is_file():
|
||||||
|
secret_value = path.read_text().strip()
|
||||||
|
if field.is_complex():
|
||||||
|
try:
|
||||||
|
secret_value = settings.__config__.parse_env_var(field.name, secret_value)
|
||||||
|
except ValueError as e:
|
||||||
|
raise SettingsError(f'error parsing env var "{env_name}"') from e
|
||||||
|
|
||||||
|
secrets[field.alias] = secret_value
|
||||||
|
else:
|
||||||
|
warnings.warn(
|
||||||
|
f'attempted to load secret file "{path}" but found a {path_type(path)} instead.',
|
||||||
|
stacklevel=4,
|
||||||
|
)
|
||||||
|
return secrets
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f'SecretsSettingsSource(secrets_dir={self.secrets_dir!r})'
|
||||||
|
|
||||||
|
|
||||||
|
def read_env_file(
|
||||||
|
file_path: StrPath, *, encoding: str = None, case_sensitive: bool = False
|
||||||
|
) -> Dict[str, Optional[str]]:
|
||||||
|
try:
|
||||||
|
from dotenv import dotenv_values
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError('python-dotenv is not installed, run `pip install pydantic[dotenv]`') from e
|
||||||
|
|
||||||
|
file_vars: Dict[str, Optional[str]] = dotenv_values(file_path, encoding=encoding or 'utf8')
|
||||||
|
if not case_sensitive:
|
||||||
|
return {k.lower(): v for k, v in file_vars.items()}
|
||||||
|
else:
|
||||||
|
return file_vars
|
||||||
|
|
||||||
|
|
||||||
|
def find_case_path(dir_path: Path, file_name: str, case_sensitive: bool) -> Optional[Path]:
|
||||||
|
"""
|
||||||
|
Find a file within path's directory matching filename, optionally ignoring case.
|
||||||
|
"""
|
||||||
|
for f in dir_path.iterdir():
|
||||||
|
if f.name == file_name:
|
||||||
|
return f
|
||||||
|
elif not case_sensitive and f.name.lower() == file_name.lower():
|
||||||
|
return f
|
||||||
|
return None
|
||||||
Reference in New Issue
Block a user