|
13 | 13 |
|
14 | 14 | import pydantic |
15 | 15 | from pydantic import Field |
| 16 | +from pydantic_core import from_json |
16 | 17 | from packaging import version |
17 | 18 | from sqlglot import exp |
18 | 19 | from sqlglot.helper import subclasses |
|
33 | 34 | field_validator, |
34 | 35 | model_validator, |
35 | 36 | validation_error_message, |
| 37 | + get_concrete_types_from_typehint, |
36 | 38 | ) |
37 | 39 | from sqlmesh.utils.aws import validate_s3_uri |
38 | 40 |
|
@@ -177,6 +179,42 @@ def get_catalog(self) -> t.Optional[str]: |
177 | 179 | return self.db |
178 | 180 | return None |
179 | 181 |
|
| 182 | + @model_validator(mode="before") |
| 183 | + @classmethod |
| 184 | + def _expand_json_strings_to_concrete_types(cls, data: t.Any) -> t.Any: |
| 185 | + """ |
| 186 | + There are situations where a connection config class has a field that is some kind of complex type |
| 187 | + (eg a list of strings or a dict) but the value is being supplied from a source such as an environment variable |
| 188 | +
|
| 189 | + When this happens, the value is supplied as a string rather than a Python object. We need some way |
| 190 | + of turning this string into the corresponding Python list or dict. |
| 191 | +
|
| 192 | + Rather than doing this piecemeal on every config subclass, this provides a generic implementatation |
| 193 | + to identify fields that may be be supplied as JSON strings and handle them transparently |
| 194 | + """ |
| 195 | + if data and isinstance(data, dict): |
| 196 | + for maybe_json_field_name in cls._get_list_and_dict_field_names(): |
| 197 | + if (value := data.get(maybe_json_field_name)) and isinstance(value, str): |
| 198 | + # crude JSON check as we dont want to try and parse every string we get |
| 199 | + value = value.strip() |
| 200 | + if value.startswith("{") or value.startswith("["): |
| 201 | + data[maybe_json_field_name] = from_json(value) |
| 202 | + |
| 203 | + return data |
| 204 | + |
| 205 | + @classmethod |
| 206 | + def _get_list_and_dict_field_names(cls) -> t.Set[str]: |
| 207 | + field_names = set() |
| 208 | + for name, field in cls.model_fields.items(): |
| 209 | + if field.annotation: |
| 210 | + field_types = get_concrete_types_from_typehint(field.annotation) |
| 211 | + |
| 212 | + # check if the field type is something that could concievably be supplied as a json string |
| 213 | + if any(ft is t for t in (list, tuple, set, dict) for ft in field_types): |
| 214 | + field_names.add(name) |
| 215 | + |
| 216 | + return field_names |
| 217 | + |
180 | 218 |
|
181 | 219 | class DuckDBAttachOptions(BaseConfig): |
182 | 220 | type: str |
|
0 commit comments