-
-
Notifications
You must be signed in to change notification settings - Fork 115
Feature: Add lazy load support in GCP #718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5cc0089
52ee47f
adaa6c2
3894a89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
|
|
||
| from ..exceptions import SettingsError | ||
| from ..utils import _lenient_issubclass | ||
| from .lazy import LazyMapping | ||
| from .types import EnvNoneType, ForceDecode, NoDecode, PathType, PydanticModel, _CliSubCommand | ||
| from .utils import ( | ||
| _annotation_is_complex, | ||
|
|
@@ -326,6 +327,7 @@ def __init__( | |
| env_ignore_empty: bool | None = None, | ||
| env_parse_none_str: str | None = None, | ||
| env_parse_enums: bool | None = None, | ||
| lazy_load: bool | None = None, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we keep this here? we agreed to enable lazy loading for GCP secret source
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I left it there so eventually is easier to implement for other sources providers and it's not a only-gcp fix. It the parameter is not passed to the base class the logic shouldn't change |
||
| ) -> None: | ||
| super().__init__(settings_cls) | ||
| self.case_sensitive = case_sensitive if case_sensitive is not None else self.config.get('case_sensitive', False) | ||
|
|
@@ -337,6 +339,7 @@ def __init__( | |
| env_parse_none_str if env_parse_none_str is not None else self.config.get('env_parse_none_str') | ||
| ) | ||
| self.env_parse_enums = env_parse_enums if env_parse_enums is not None else self.config.get('env_parse_enums') | ||
| self.lazy_load = lazy_load if lazy_load is not None else self.config.get('lazy_load', False) | ||
|
|
||
| def _apply_case_sensitive(self, value: str) -> str: | ||
| return value.lower() if not self.case_sensitive else value | ||
|
|
@@ -510,6 +513,14 @@ def _get_resolved_field_value(self, field: FieldInfo, field_name: str) -> tuple[ | |
| return field_value, field_key, value_is_complex | ||
|
|
||
| def __call__(self) -> dict[str, Any]: | ||
| # If lazy loading is enabled, defer field resolution to access time | ||
| if self.lazy_load: | ||
| # Store the LazyMapping on the source for later retrieval | ||
| self._lazy_mapping = LazyMapping(self) | ||
| # Return empty dict to avoid eager evaluation during initialization | ||
| return {} | ||
|
|
||
| # Otherwise, use eager field loading | ||
| data: dict[str, Any] = {} | ||
|
|
||
| for field_name, field in self.settings_cls.model_fields.items(): | ||
|
|
@@ -541,14 +552,14 @@ def __call__(self) -> dict[str, Any]: | |
| data[field_key] = self._replace_field_names_case_insensitively(field, field_value) | ||
| else: | ||
| data[field_key] = field_value | ||
|
|
||
| return data | ||
|
|
||
|
|
||
| __all__ = [ | ||
| 'ConfigFileSourceMixin', | ||
| 'DefaultSettingsSource', | ||
| 'InitSettingsSource', | ||
| 'LazyMapping', | ||
| 'PydanticBaseEnvSettingsSource', | ||
| 'PydanticBaseSettingsSource', | ||
| 'SettingsError', | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| """Lazy loading support for settings sources.""" | ||
|
|
||
| from __future__ import annotations as _annotations | ||
|
|
||
| from collections.abc import Iterator, Mapping | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| from pydantic.fields import FieldInfo | ||
|
|
||
| from ..exceptions import SettingsError | ||
| from .utils import _get_alias_names | ||
|
|
||
| if TYPE_CHECKING: | ||
| from .base import PydanticBaseEnvSettingsSource | ||
|
|
||
|
|
||
| class LazyMapping(Mapping[str, Any]): | ||
| """Dict-like mapping that defers field value resolution until keys are accessed.""" | ||
|
|
||
| def __init__(self, source: PydanticBaseEnvSettingsSource) -> None: | ||
| """Initialize with a source instance that will compute values on demand.""" | ||
| self._source = source | ||
| self._cached_values: dict[str, Any] = {} | ||
|
|
||
| def __getitem__(self, key: str) -> Any: | ||
| """Get a field value, computing it lazily on first access.""" | ||
| # Return cached value if available | ||
| if key in self._cached_values: | ||
| return self._cached_values[key] | ||
|
|
||
| # Find the field in the settings class | ||
| field_name: str | None = None | ||
| field_info: FieldInfo | None = None | ||
|
|
||
| for fname, finfo in self._source.settings_cls.model_fields.items(): | ||
| alias_names, *_ = _get_alias_names(fname, finfo) | ||
| if key in alias_names or key == fname: | ||
| field_name = fname | ||
| field_info = finfo | ||
| break | ||
|
|
||
| if field_name is None or field_info is None: | ||
| raise KeyError(key) | ||
|
|
||
| # Resolve and cache the field value | ||
| try: | ||
| field_value, _, value_is_complex = self._source._get_resolved_field_value(field_info, field_name) | ||
| prepared_value = self._source.prepare_field_value(field_name, field_info, field_value, value_is_complex) | ||
| self._cached_values[key] = prepared_value | ||
| return prepared_value | ||
| except Exception as e: | ||
| raise SettingsError( | ||
| f'error getting value for field "{field_name}" from source "{self._source.__class__.__name__}"' | ||
| ) from e | ||
|
|
||
| def __iter__(self) -> Iterator[str]: | ||
| """Iterate over all possible field keys.""" | ||
| seen: set[str] = set() | ||
| for field_name, field_info in self._source.settings_cls.model_fields.items(): | ||
| alias_names, *_ = _get_alias_names(field_name, field_info) | ||
| for alias in alias_names: | ||
| if alias not in seen: | ||
| seen.add(alias) | ||
| yield alias | ||
| if field_name not in seen: | ||
| yield field_name | ||
|
|
||
| def __len__(self) -> int: | ||
| """Return the count of fields in the settings class.""" | ||
| return len(self._source.settings_cls.model_fields) | ||
|
|
||
| def copy(self) -> LazyMapping: | ||
| """Return a copy to preserve lazy behavior through Pydantic's deep_update().""" | ||
| new_mapping = LazyMapping(self._source) | ||
| new_mapping._cached_values = self._cached_values.copy() | ||
| return new_mapping | ||
|
|
||
|
|
||
| __all__ = ['LazyMapping'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two questions:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_field_valuemethod inEnvSettingsSource, thefield_valuewill not be found and will return NonePS: left a fix with an issue with the model dump that wasn't loading the lazy fields
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean if the value is not a valid value for the field. like you defined an
intfield but the value is string. or you put some limitation on the string length.