Skip to content

Commit 5cecc63

Browse files
fix encoder issues
1 parent 1283488 commit 5cecc63

File tree

5 files changed

+82
-57
lines changed

5 files changed

+82
-57
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from types import UnionType
2+
from typing import Any, Literal, get_args, get_origin
3+
4+
from pydantic.fields import FieldInfo
5+
6+
7+
def get_type(info: FieldInfo) -> Any:
8+
field_type = info.annotation
9+
if args := get_args(info.annotation):
10+
field_type = next(a for a in args if a != type(None))
11+
return field_type
12+
13+
14+
def is_literal(info: FieldInfo) -> bool:
15+
origin = get_origin(info.annotation)
16+
return origin is Literal
17+
18+
19+
def is_nullable(info: FieldInfo) -> bool:
20+
origin = get_origin(info.annotation) # X | None or Optional[X] will return Union
21+
if origin is UnionType:
22+
return any(x in get_args(info.annotation) for x in (type(None), Any))
23+
return False
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Any
2+
3+
from models_library.utils.pydantic_fields_extension import get_type
4+
from pydantic import BaseModel, SecretStr
5+
6+
7+
def model_dump_with_secrets(
8+
settings_obj: BaseModel, show_secrets: bool, **pydantic_export_options
9+
) -> dict[str, Any]:
10+
data = settings_obj.model_dump(**pydantic_export_options)
11+
12+
for field_name in settings_obj.model_fields:
13+
field_data = data[field_name]
14+
15+
if isinstance(field_data, SecretStr):
16+
if show_secrets:
17+
data[field_name] = field_data.get_secret_value() # Expose the raw value
18+
else:
19+
data[field_name] = str(field_data)
20+
elif isinstance(field_data, dict):
21+
field_type = get_type(settings_obj.model_fields[field_name])
22+
if issubclass(field_type, BaseModel):
23+
data[field_name] = model_dump_with_secrets(
24+
field_type.model_validate(field_data),
25+
show_secrets,
26+
**pydantic_export_options,
27+
)
28+
29+
return data

packages/service-library/tests/deferred_tasks/test_deferred_tasks.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import psutil
1717
import pytest
1818
from aiohttp.test_utils import unused_port
19+
from models_library.utils.json_serialization import json_dumps
20+
from models_library.utils.serialization import model_dump_with_secrets
1921
from pydantic import NonNegativeFloat, NonNegativeInt
2022
from pytest_mock import MockerFixture
2123
from servicelib import redis as servicelib_redis
@@ -24,7 +26,6 @@
2426
from servicelib.sequences_utils import partition_gen
2527
from settings_library.rabbit import RabbitSettings
2628
from settings_library.redis import RedisSettings
27-
from settings_library.utils_encoders import create_json_encoder_wo_secrets
2829
from tenacity.asyncio import AsyncRetrying
2930
from tenacity.retry import retry_if_exception_type
3031
from tenacity.stop import stop_after_delay
@@ -123,7 +124,6 @@ async def _tcp_command(
123124

124125
def _get_serialization_options() -> dict[str, Any]:
125126
return {
126-
"encoder": create_json_encoder_wo_secrets(RabbitSettings),
127127
"exclude_defaults": True,
128128
"exclude_none": True,
129129
"exclude_unset": True,
@@ -158,8 +158,20 @@ async def start(self) -> None:
158158
response = await _tcp_command(
159159
"init-context",
160160
{
161-
"rabbit": self.rabbit_service.model_dump_json(**_get_serialization_options()),
162-
"redis": self.redis_service.model_dump_json(**_get_serialization_options()),
161+
"rabbit": json_dumps(
162+
model_dump_with_secrets(
163+
self.rabbit_service,
164+
show_secrets=True,
165+
**_get_serialization_options(),
166+
)
167+
),
168+
"redis": json_dumps(
169+
model_dump_with_secrets(
170+
self.redis_service,
171+
show_secrets=True,
172+
**_get_serialization_options(),
173+
)
174+
),
163175
"max-workers": self.max_workers,
164176
},
165177
port=self.remote_process.port,

packages/settings-library/src/settings_library/base.py

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import logging
22
from functools import cached_property
3-
from types import UnionType
4-
from typing import Any, Final, Literal, get_args, get_origin
3+
from typing import Any, Final, get_origin
54

5+
from models_library.utils.pydantic_fields_extension import (
6+
get_type,
7+
is_literal,
8+
is_nullable,
9+
)
610
from pydantic import ValidationInfo, field_validator
711
from pydantic.fields import FieldInfo
812
from pydantic_core import PydanticUndefined, ValidationError
@@ -21,38 +25,19 @@ def __init__(self, errors):
2125
self.errors = errors
2226

2327

24-
def _allows_none(info: FieldInfo) -> bool:
25-
origin = get_origin(info.annotation) # X | None or Optional[X] will return Union
26-
if origin is UnionType:
27-
return any(x in get_args(info.annotation) for x in (type(None), Any))
28-
return False
29-
30-
31-
def _get_type(info: FieldInfo) -> Any:
32-
field_type = info.annotation
33-
if args := get_args(info.annotation):
34-
field_type = next(a for a in args if a != type(None))
35-
return field_type
36-
37-
38-
def _is_literal(info: FieldInfo) -> bool:
39-
origin = get_origin(info.annotation)
40-
return origin is Literal
41-
42-
4328
def _create_settings_from_env(field_name: str, info: FieldInfo):
4429
# NOTE: Cannot pass only field.type_ because @prepare_field (when this function is called)
4530
# this value is still not resolved (field.type_ at that moment has a weak_ref).
4631
# Therefore we keep the entire 'field' but MUST be treated here as read-only
4732

4833
def _default_factory():
4934
"""Creates default from sub-settings or None (if nullable)"""
50-
field_settings_cls = _get_type(info)
35+
field_settings_cls = get_type(info)
5136
try:
5237
return field_settings_cls()
5338

5439
except ValidationError as err:
55-
if _allows_none(info):
40+
if is_nullable(info):
5641
# e.g. Optional[PostgresSettings] would warn if defaults to None
5742
_logger.warning(
5843
_DEFAULTS_TO_NONE_MSG,
@@ -80,7 +65,7 @@ def _parse_none(cls, v, info: ValidationInfo):
8065
# WARNING: In nullable fields, envs equal to null or none are parsed as None !!
8166
if (
8267
info.field_name
83-
and _allows_none(cls.model_fields[info.field_name])
68+
and is_nullable(cls.model_fields[info.field_name])
8469
and isinstance(v, str)
8570
and v.lower() in ("none",)
8671
):
@@ -107,13 +92,13 @@ def __pydantic_init_subclass__(cls, **kwargs: Any):
10792
"auto_default_from_env", False
10893
)
10994
)
110-
field_type = _get_type(field)
95+
field_type = get_type(field)
11196

11297
# Avoids issubclass raising TypeError. SEE test_issubclass_type_error_with_pydantic_models
11398
is_not_composed = (
11499
get_origin(field_type) is None
115100
) # is not composed as dict[str, Any] or Generic[Base]
116-
is_not_literal = not _is_literal(field)
101+
is_not_literal = not is_literal(field)
117102

118103
if (
119104
is_not_literal

packages/settings-library/src/settings_library/utils_cli.py

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,12 @@
77

88
import rich
99
import typer
10-
from pydantic import SecretStr, ValidationError
10+
from models_library.utils.serialization import model_dump_with_secrets
11+
from pydantic import ValidationError
1112
from pydantic_settings import BaseSettings
1213

1314
from ._constants import HEADER_STR
14-
from .base import BaseCustomSettings, _get_type
15-
16-
17-
def model_dump_with_secrets(
18-
settings_obj: BaseSettings, show_secret: bool, **pydantic_export_options
19-
) -> dict[str, Any]:
20-
data = settings_obj.model_dump(**pydantic_export_options)
21-
22-
for field_name in settings_obj.model_fields:
23-
field_data = data[field_name]
24-
25-
if isinstance(field_data, SecretStr):
26-
if show_secret:
27-
data[field_name] = field_data.get_secret_value() # Expose the raw value
28-
else:
29-
data[field_name] = str(field_data)
30-
elif isinstance(field_data, dict):
31-
field_type = _get_type(settings_obj.model_fields[field_name])
32-
if issubclass(field_type, BaseSettings):
33-
data[field_name] = model_dump_with_secrets(
34-
field_type.model_validate(field_data),
35-
show_secret,
36-
**pydantic_export_options,
37-
)
38-
39-
return data
15+
from .base import BaseCustomSettings
4016

4117

4218
def print_as_envfile(

0 commit comments

Comments
 (0)