Skip to content

Commit 52fd1e2

Browse files
committed
[secret-resource-param] core
1 parent 4515040 commit 52fd1e2

7 files changed

Lines changed: 72 additions & 3 deletions

File tree

python_modules/dagster/dagster/_config/field.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ class Field:
237237
description (str):
238238
A human-readable description of this config field.
239239
240+
is_secret (bool):
241+
Whether this field contains sensitive data that should be masked in UIs. Defaults to False.
242+
240243
Examples:
241244
.. code-block:: python
242245
@@ -268,13 +271,15 @@ def __init__(
268271
default_value: Any = FIELD_NO_DEFAULT_PROVIDED,
269272
is_required: Optional[bool] = None,
270273
description: Optional[str] = None,
274+
is_secret: bool = False,
271275
):
272276
from dagster._config.post_process import resolve_defaults
273277
from dagster._config.validate import validate_config
274278

275279
self.config_type = check.inst(self._resolve_config_arg(config), ConfigType)
276280

277281
self._description = check.opt_str_param(description, "description")
282+
self._is_secret = check.bool_param(is_secret, "is_secret")
278283

279284
check.opt_bool_param(is_required, "is_required")
280285

@@ -367,6 +372,12 @@ def description(self) -> Optional[str]:
367372
"""A human-readable description of this config field, if provided."""
368373
return self._description
369374

375+
@public
376+
@property
377+
def is_secret(self) -> bool:
378+
"""Whether this field contains sensitive data that should be masked in UIs."""
379+
return self._is_secret
380+
370381
@property
371382
def default_value_as_json_str(self) -> str:
372383
check.invariant(self.default_provided, "Asking for default value when none was provided")

python_modules/dagster/dagster/_config/pythonic_config/conversion_utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,18 @@ def _convert_pydantic_field(
144144
if isinstance(default_to_pass, Enum):
145145
default_to_pass = default_to_pass.name
146146

147+
# Extract is_secret from json_schema_extra
148+
extras = pydantic_field.json_schema_extra or {}
149+
is_secret = bool(extras.get("is_secret", False))
150+
147151
return Field(
148152
config=config_type,
149153
description=pydantic_field.description,
150154
is_required=pydantic_field.is_required()
151155
and not is_closed_python_optional_type(field_type)
152156
and default_to_pass == FIELD_NO_DEFAULT_PROVIDED,
153157
default_value=default_to_pass,
158+
is_secret=is_secret,
154159
)
155160

156161

python_modules/dagster/dagster/_config/snap.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ class ConfigEnumValueSnap:
169169
description: Optional[str]
170170

171171

172-
@whitelist_for_serdes
172+
@whitelist_for_serdes(skip_when_none_fields={"is_secret"})
173173
@record
174174
class ConfigFieldSnap:
175175
name: Optional[str]
@@ -178,6 +178,7 @@ class ConfigFieldSnap:
178178
default_provided: bool
179179
default_value_as_json_str: Optional[str]
180180
description: Optional[str]
181+
is_secret: Optional[bool] = None
181182

182183

183184
def snap_from_field(name: str, field: Field):
@@ -190,6 +191,7 @@ def snap_from_field(name: str, field: Field):
190191
field.default_value_as_json_str if field.default_provided else None
191192
),
192193
description=field.description,
194+
is_secret=field.is_secret if field.is_secret else None,
193195
)
194196

195197

python_modules/dagster/dagster_tests/core_tests/config_types_tests/test_config_type_system.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,3 +1188,14 @@ def test_order(context):
11881188
assert wrap_op_in_graph_and_execute(
11891189
test_order, run_config={"ops": {"test_order": {"config": alphabet}}}
11901190
).success
1191+
1192+
1193+
def test_field_is_secret_parameter():
1194+
"""Test that Field accepts and stores is_secret parameter."""
1195+
# Non-secret field
1196+
regular_field = dg.Field(str, description="A regular field")
1197+
assert regular_field.is_secret is False
1198+
1199+
# Secret field
1200+
secret_field = dg.Field(str, description="A secret field", is_secret=True)
1201+
assert secret_field.is_secret is True

python_modules/dagster/dagster_tests/core_tests/resource_tests/pythonic_resources/test_general_pythonic_resources.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
InitResourceContext,
1414
)
1515
from dagster._check import CheckError
16-
from dagster._config.pythonic_config import ConfigurableResourceFactory
16+
from dagster._config.pythonic_config import (
17+
ConfigurableResourceFactory,
18+
infer_schema_from_config_class,
19+
)
1720
from dagster._utils.cached_method import cached_method
1821
from pydantic import (
1922
Field as PyField,
@@ -1119,3 +1122,33 @@ class MergeResource(dg.ConfigurableResource):
11191122
int_res=StrResource.configure_at_launch(),
11201123
str_res=IntResource.configure_at_launch(),
11211124
)
1125+
1126+
1127+
def test_secret_field():
1128+
"""Test that is_secret is extracted from json_schema_extra in ConfigurableResource."""
1129+
1130+
class MyResource(dg.ConfigurableResource):
1131+
api_key: str = PyField(
1132+
description="API key for authentication",
1133+
json_schema_extra={"is_secret": True},
1134+
)
1135+
host: str = PyField(description="Host URL")
1136+
password: str = PyField(json_schema_extra={"is_secret": True})
1137+
1138+
# Get the inferred schema
1139+
schema_field = infer_schema_from_config_class(MyResource)
1140+
1141+
# The schema should have the fields
1142+
fields_dict = schema_field.config_type.fields # type: ignore
1143+
1144+
# Check that api_key is marked as secret
1145+
assert "api_key" in fields_dict
1146+
assert fields_dict["api_key"].is_secret is True
1147+
1148+
# Check that host is not marked as secret
1149+
assert "host" in fields_dict
1150+
assert fields_dict["host"].is_secret is False
1151+
1152+
# Check that password is marked as secret
1153+
assert "password" in fields_dict
1154+
assert fields_dict["password"].is_secret is True

python_modules/dagster/dagster_tests/core_tests/snap_tests/test_config_schema_snapshot.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,11 @@ def test_field_things():
6767
"opt": dg.Field(int, is_required=False),
6868
"opt_with_default": dg.Field(int, is_required=False, default_value=2),
6969
"req_with_desc": dg.Field(int, description="A desc"),
70+
"secret_field": dg.Field(str, is_secret=True),
7071
}
7172
)
7273

73-
assert dict_snap.fields and len(dict_snap.fields) == 4
74+
assert dict_snap.fields and len(dict_snap.fields) == 5
7475

7576
field_snap_dict = {field_snap.name: field_snap for field_snap in dict_snap.fields}
7677

@@ -85,6 +86,8 @@ def test_field_things():
8586

8687
assert field_snap_dict["req_with_desc"].is_required is True
8788
assert field_snap_dict["req_with_desc"].description == "A desc"
89+
assert field_snap_dict["secret_field"].is_secret is True
90+
assert not field_snap_dict["req"].is_secret
8891

8992

9093
def test_basic_list():

python_modules/libraries/dagster-shared/dagster_shared/dagster_model/pydantic_compat_layer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def discriminator(self) -> Optional[str]:
6060
if hasattr(self.field, "discriminator"):
6161
return self.field.discriminator if hasattr(self.field, "discriminator") else None
6262

63+
@property
64+
def json_schema_extra(self) -> Optional[dict[str, Any]]:
65+
return getattr(self.field, "json_schema_extra", None)
66+
6367

6468
def model_fields(model: type[BaseModel]) -> dict[str, ModelFieldCompat]:
6569
"""Returns a dictionary of fields for a given pydantic model, wrapped

0 commit comments

Comments
 (0)