sqlmesh.utils.pydantic
1from __future__ import annotations 2 3import json 4import typing as t 5from datetime import tzinfo 6 7import pydantic 8from pydantic import ValidationInfo as ValidationInfo 9from pydantic.fields import FieldInfo 10from sqlglot import exp, parse_one 11from sqlglot.helper import ensure_list 12from sqlglot.optimizer.normalize_identifiers import normalize_identifiers 13from sqlglot.optimizer.qualify_columns import quote_identifiers 14 15from sqlmesh.core import dialect as d 16from sqlmesh.utils import str_to_bool 17 18if t.TYPE_CHECKING: 19 from sqlglot._typing import E 20 21 Model = t.TypeVar("Model", bound="PydanticModel") 22 23 24T = t.TypeVar("T") 25DEFAULT_ARGS = {"exclude_none": True, "by_alias": True} 26PRIVATE_FIELDS = "__pydantic_private__" 27PYDANTIC_MAJOR_VERSION, PYDANTIC_MINOR_VERSION = [int(p) for p in pydantic.__version__.split(".")][ 28 :2 29] 30 31 32def field_validator(*args: t.Any, **kwargs: t.Any) -> t.Callable[[t.Any], t.Any]: 33 return pydantic.field_validator(*args, **kwargs) 34 35 36def model_validator(*args: t.Any, **kwargs: t.Any) -> t.Callable[[t.Any], t.Any]: 37 return pydantic.model_validator(*args, **kwargs) 38 39 40def field_serializer(*args: t.Any, **kwargs: t.Any) -> t.Callable[[t.Any], t.Any]: 41 return pydantic.field_serializer(*args, **kwargs) 42 43 44def validation_data(info_or_data: t.Any) -> t.Dict[str, t.Any]: 45 """Safely extract the validated-data dict from a ValidationInfo, dict, or None. 46 47 Pydantic 2.13+ sets ValidationInfo.data to None during model_validate_json(). 48 This normalizes all inputs to a dict, returning an empty dict when data is unavailable. 49 """ 50 if isinstance(info_or_data, dict): 51 return info_or_data 52 if info_or_data is not None: 53 return info_or_data.data or {} 54 return {} 55 56 57def get_dialect(values: t.Any) -> str: 58 """Extracts dialect from a dict or pydantic obj, defaulting to the globally set dialect. 59 60 Python models allow users to instantiate pydantic models by hand. This is problematic 61 because the validators kick in with the SQLGLot dialect. To instantiate Pydantic Models used 62 in python models using the project default dialect, we set a class variable on the model 63 registry and use that here. 64 """ 65 66 from sqlmesh.core.model import model 67 68 dialect = validation_data(values).get("dialect") 69 return model._dialect if dialect is None else dialect # type: ignore 70 71 72def _expression_encoder(e: exp.Expr) -> str: 73 return e.meta.get("sql") or e.sql(dialect=e.meta.get("dialect")) 74 75 76AuditQueryTypes = t.Union[exp.Query, d.JinjaQuery] 77ModelQueryTypes = t.Union[exp.Query, d.JinjaQuery, d.MacroFunc] 78 79 80class PydanticModel(pydantic.BaseModel): 81 model_config = pydantic.ConfigDict( 82 # Even though Pydantic v2 kept support for json_encoders, the functionality has been 83 # crippled badly. Here we need to enumerate all different ways of how sqlglot expressions 84 # show up in pydantic models. 85 json_encoders={ 86 exp.Expr: _expression_encoder, 87 exp.DataType: _expression_encoder, 88 exp.Tuple: _expression_encoder, 89 AuditQueryTypes: _expression_encoder, # type: ignore 90 ModelQueryTypes: _expression_encoder, # type: ignore 91 tzinfo: lambda tz: tz.key, 92 }, 93 arbitrary_types_allowed=True, 94 extra="forbid", 95 protected_namespaces=(), 96 ) 97 98 _hash_func_mapping: t.ClassVar[t.Dict[t.Type[t.Any], t.Callable[[t.Any], int]]] = {} 99 100 def dict(self, **kwargs: t.Any) -> t.Dict[str, t.Any]: 101 kwargs = {**DEFAULT_ARGS, **kwargs} 102 return super().model_dump(**kwargs) # type: ignore 103 104 def json( 105 self, 106 **kwargs: t.Any, 107 ) -> str: 108 kwargs = {**DEFAULT_ARGS, **kwargs} 109 # Pydantic v2 doesn't support arbitrary arguments for json.dump(). 110 if kwargs.pop("sort_keys", False): 111 return json.dumps(super().model_dump(mode="json", **kwargs), sort_keys=True) 112 113 return super().model_dump_json(**kwargs) 114 115 def copy(self: "Model", **kwargs: t.Any) -> "Model": 116 return super().model_copy(**kwargs) 117 118 @property 119 def fields_set(self: "Model") -> t.Set[str]: 120 return self.__pydantic_fields_set__ 121 122 @classmethod 123 def parse_obj(cls: t.Type["Model"], obj: t.Any) -> "Model": 124 return super().model_validate(obj) 125 126 @classmethod 127 def parse_raw(cls: t.Type["Model"], b: t.Union[str, bytes], **kwargs: t.Any) -> "Model": 128 return super().model_validate_json(b, **kwargs) 129 130 @classmethod 131 def missing_required_fields( 132 cls: t.Type["PydanticModel"], provided_fields: t.Set[str] 133 ) -> t.Set[str]: 134 return cls.required_fields() - provided_fields 135 136 @classmethod 137 def extra_fields(cls: t.Type["PydanticModel"], provided_fields: t.Set[str]) -> t.Set[str]: 138 return provided_fields - cls.all_fields() 139 140 @classmethod 141 def all_fields(cls: t.Type["PydanticModel"]) -> t.Set[str]: 142 return cls._fields() 143 144 @classmethod 145 def all_field_infos(cls: t.Type["PydanticModel"]) -> t.Dict[str, FieldInfo]: 146 return cls.model_fields 147 148 @classmethod 149 def required_fields(cls: t.Type["PydanticModel"]) -> t.Set[str]: 150 return cls._fields(lambda field: field.is_required()) 151 152 @classmethod 153 def _fields( 154 cls: t.Type["PydanticModel"], 155 predicate: t.Callable[[t.Any], bool] = lambda _: True, 156 ) -> t.Set[str]: 157 return { 158 field_info.alias if field_info.alias else field_name 159 for field_name, field_info in cls.all_field_infos().items() 160 if predicate(field_info) 161 } 162 163 def __eq__(self, other: t.Any) -> bool: 164 if (PYDANTIC_MAJOR_VERSION, PYDANTIC_MINOR_VERSION) < (2, 6): 165 if isinstance(other, pydantic.BaseModel): 166 return self.dict() == other.dict() 167 return self.dict() == other 168 return super().__eq__(other) 169 170 def __hash__(self) -> int: 171 if (PYDANTIC_MAJOR_VERSION, PYDANTIC_MINOR_VERSION) < (2, 6): 172 obj = {k: v for k, v in self.__dict__.items() if k in self.all_field_infos()} 173 return hash(self.__class__) + hash(tuple(obj.values())) 174 175 from pydantic._internal._model_construction import make_hash_func # type: ignore 176 177 if self.__class__ not in PydanticModel._hash_func_mapping: 178 PydanticModel._hash_func_mapping[self.__class__] = make_hash_func(self.__class__) 179 180 return PydanticModel._hash_func_mapping[self.__class__](self) 181 182 def __str__(self) -> str: 183 args = [] 184 185 for k, info in self.all_field_infos().items(): 186 v = getattr(self, k) 187 188 if type(v) != type(info.default) or v != info.default: 189 args.append(f"{k}: {v}") 190 191 return f"{self.__class__.__name__}<{', '.join(args)}>" 192 193 def __repr__(self) -> str: 194 return str(self) 195 196 197def validate_list_of_strings(v: t.Any) -> t.List[str]: 198 if isinstance(v, exp.Identifier): 199 return [v.name] 200 if isinstance(v, (exp.Tuple, exp.Array)): 201 return [e.name for e in v.expressions] 202 return [i.name if isinstance(i, exp.Identifier) else str(i) for i in v] 203 204 205def validate_string(v: t.Any) -> str: 206 if isinstance(v, exp.Expr): 207 return v.name 208 return str(v) 209 210 211def validate_expression(expression: E, dialect: str) -> E: 212 # this normalizes and quotes identifiers in the given expression according the specified dialect 213 # it also sets expression.meta["dialect"] so that when we serialize for state, the expression is serialized in the correct dialect 214 return _get_field(expression, {"dialect": dialect}) # type: ignore 215 216 217def bool_validator(v: t.Any) -> bool: 218 if isinstance(v, exp.Boolean): 219 return v.this 220 if isinstance(v, exp.Expr): 221 return str_to_bool(v.name) 222 return str_to_bool(str(v or "")) 223 224 225def positive_int_validator(v: t.Any) -> int: 226 if isinstance(v, exp.Expr) and v.is_int: 227 v = int(v.name) 228 if not isinstance(v, int): 229 raise ValueError(f"Invalid num {v}. Value must be an integer value") 230 if v <= 0: 231 raise ValueError(f"Invalid num {v}. Value must be a positive integer") 232 return v 233 234 235def validation_error_message(error: pydantic.ValidationError, base: str) -> str: 236 errors = "\n ".join(_formatted_validation_errors(error)) 237 return f"{base}\n {errors}" 238 239 240def _formatted_validation_errors(error: pydantic.ValidationError) -> t.List[str]: 241 result = [] 242 for e in error.errors(): 243 msg = e["msg"] 244 loc: t.Optional[t.Tuple] = e.get("loc") 245 loc_str = ".".join(loc) if loc else None 246 result.append(f"Invalid field '{loc_str}':\n {msg}" if loc_str else msg) 247 return result 248 249 250def _get_field( 251 v: t.Any, 252 values: t.Any, 253) -> exp.Expr: 254 dialect = get_dialect(values) 255 256 if isinstance(v, exp.Expr): 257 expression = v 258 else: 259 expression = parse_one(v, dialect=dialect) 260 261 expression = exp.column(expression) if isinstance(expression, exp.Identifier) else expression 262 expression = quote_identifiers( 263 normalize_identifiers(expression, dialect=dialect), dialect=dialect 264 ) 265 expression.meta["dialect"] = dialect 266 267 return expression 268 269 270def _get_fields( 271 v: t.Any, 272 values: t.Any, 273) -> t.List[exp.Expr]: 274 dialect = get_dialect(values) 275 276 if isinstance(v, (exp.Tuple, exp.Array)): 277 expressions: t.List[exp.Expr] = v.expressions 278 elif isinstance(v, exp.Expr): 279 expressions = [v] 280 else: 281 items: t.List[t.Any] = ensure_list(v) 282 expressions = [ 283 parse_one(entry, dialect=dialect) if isinstance(entry, str) else entry # type: ignore[misc] 284 for entry in items 285 ] 286 287 results = [] 288 289 for expr in expressions: 290 results.append(_get_field(expr, values)) 291 292 return results 293 294 295def list_of_fields_validator(v: t.Any, values: t.Any) -> t.List[exp.Expr]: 296 return _get_fields(v, values) 297 298 299def column_validator(v: t.Any, values: t.Any) -> exp.Column: 300 expression = _get_field(v, values) 301 if not isinstance(expression, exp.Column): 302 raise ValueError(f"Invalid column {expression}. Value must be a column") 303 return expression 304 305 306def list_of_fields_or_star_validator( 307 v: t.Any, values: t.Any 308) -> t.Union[exp.Star, t.List[exp.Expr]]: 309 expressions = _get_fields(v, values) 310 if len(expressions) == 1 and isinstance(expressions[0], exp.Star): 311 return t.cast(exp.Star, expressions[0]) 312 return t.cast(t.List[exp.Expr], expressions) 313 314 315def cron_validator(v: t.Any) -> str: 316 if isinstance(v, exp.Expr): 317 v = v.name 318 319 from croniter import CroniterBadCronError, croniter 320 321 if not isinstance(v, str): 322 raise ValueError(f"Invalid cron expression '{v}'. Value must be a string.") 323 324 try: 325 croniter(v) 326 except CroniterBadCronError: 327 raise ValueError(f"Invalid cron expression '{v}'") 328 return v 329 330 331def get_concrete_types_from_typehint(typehint: type[t.Any]) -> set[type[t.Any]]: 332 concrete_types = set() 333 unpacked = t.get_origin(typehint) 334 if unpacked is None: 335 if type(typehint) == type(type): 336 return {typehint} 337 elif unpacked is t.Union: 338 for item in t.get_args(typehint): 339 if str(item).startswith("typing."): 340 concrete_types |= get_concrete_types_from_typehint(item) 341 else: 342 concrete_types.add(item) 343 else: 344 concrete_types.add(unpacked) 345 346 return concrete_types 347 348 349if t.TYPE_CHECKING: 350 SQLGlotListOfStrings = t.List[str] 351 SQLGlotString = str 352 SQLGlotBool = bool 353 SQLGlotPositiveInt = int 354 SQLGlotColumn = exp.Column 355 SQLGlotListOfFields = t.List[exp.Expr] 356 SQLGlotListOfFieldsOrStar = t.Union[SQLGlotListOfFields, exp.Star] 357 SQLGlotCron = str 358else: 359 from pydantic.functional_validators import BeforeValidator 360 361 SQLGlotListOfStrings = t.Annotated[t.List[str], BeforeValidator(validate_list_of_strings)] 362 SQLGlotString = t.Annotated[str, BeforeValidator(validate_string)] 363 SQLGlotBool = t.Annotated[bool, BeforeValidator(bool_validator)] 364 SQLGlotPositiveInt = t.Annotated[int, BeforeValidator(positive_int_validator)] 365 SQLGlotColumn = t.Annotated[exp.Expr, BeforeValidator(column_validator)] 366 SQLGlotListOfFields = t.Annotated[t.List[exp.Expr], BeforeValidator(list_of_fields_validator)] 367 SQLGlotListOfFieldsOrStar = t.Annotated[ 368 t.Union[SQLGlotListOfFields, exp.Star], BeforeValidator(list_of_fields_or_star_validator) 369 ] 370 SQLGlotCron = t.Annotated[str, BeforeValidator(cron_validator)]
45def validation_data(info_or_data: t.Any) -> t.Dict[str, t.Any]: 46 """Safely extract the validated-data dict from a ValidationInfo, dict, or None. 47 48 Pydantic 2.13+ sets ValidationInfo.data to None during model_validate_json(). 49 This normalizes all inputs to a dict, returning an empty dict when data is unavailable. 50 """ 51 if isinstance(info_or_data, dict): 52 return info_or_data 53 if info_or_data is not None: 54 return info_or_data.data or {} 55 return {}
Safely extract the validated-data dict from a ValidationInfo, dict, or None.
Pydantic 2.13+ sets ValidationInfo.data to None during model_validate_json(). This normalizes all inputs to a dict, returning an empty dict when data is unavailable.
58def get_dialect(values: t.Any) -> str: 59 """Extracts dialect from a dict or pydantic obj, defaulting to the globally set dialect. 60 61 Python models allow users to instantiate pydantic models by hand. This is problematic 62 because the validators kick in with the SQLGLot dialect. To instantiate Pydantic Models used 63 in python models using the project default dialect, we set a class variable on the model 64 registry and use that here. 65 """ 66 67 from sqlmesh.core.model import model 68 69 dialect = validation_data(values).get("dialect") 70 return model._dialect if dialect is None else dialect # type: ignore
Extracts dialect from a dict or pydantic obj, defaulting to the globally set dialect.
Python models allow users to instantiate pydantic models by hand. This is problematic because the validators kick in with the SQLGLot dialect. To instantiate Pydantic Models used in python models using the project default dialect, we set a class variable on the model registry and use that here.
81class PydanticModel(pydantic.BaseModel): 82 model_config = pydantic.ConfigDict( 83 # Even though Pydantic v2 kept support for json_encoders, the functionality has been 84 # crippled badly. Here we need to enumerate all different ways of how sqlglot expressions 85 # show up in pydantic models. 86 json_encoders={ 87 exp.Expr: _expression_encoder, 88 exp.DataType: _expression_encoder, 89 exp.Tuple: _expression_encoder, 90 AuditQueryTypes: _expression_encoder, # type: ignore 91 ModelQueryTypes: _expression_encoder, # type: ignore 92 tzinfo: lambda tz: tz.key, 93 }, 94 arbitrary_types_allowed=True, 95 extra="forbid", 96 protected_namespaces=(), 97 ) 98 99 _hash_func_mapping: t.ClassVar[t.Dict[t.Type[t.Any], t.Callable[[t.Any], int]]] = {} 100 101 def dict(self, **kwargs: t.Any) -> t.Dict[str, t.Any]: 102 kwargs = {**DEFAULT_ARGS, **kwargs} 103 return super().model_dump(**kwargs) # type: ignore 104 105 def json( 106 self, 107 **kwargs: t.Any, 108 ) -> str: 109 kwargs = {**DEFAULT_ARGS, **kwargs} 110 # Pydantic v2 doesn't support arbitrary arguments for json.dump(). 111 if kwargs.pop("sort_keys", False): 112 return json.dumps(super().model_dump(mode="json", **kwargs), sort_keys=True) 113 114 return super().model_dump_json(**kwargs) 115 116 def copy(self: "Model", **kwargs: t.Any) -> "Model": 117 return super().model_copy(**kwargs) 118 119 @property 120 def fields_set(self: "Model") -> t.Set[str]: 121 return self.__pydantic_fields_set__ 122 123 @classmethod 124 def parse_obj(cls: t.Type["Model"], obj: t.Any) -> "Model": 125 return super().model_validate(obj) 126 127 @classmethod 128 def parse_raw(cls: t.Type["Model"], b: t.Union[str, bytes], **kwargs: t.Any) -> "Model": 129 return super().model_validate_json(b, **kwargs) 130 131 @classmethod 132 def missing_required_fields( 133 cls: t.Type["PydanticModel"], provided_fields: t.Set[str] 134 ) -> t.Set[str]: 135 return cls.required_fields() - provided_fields 136 137 @classmethod 138 def extra_fields(cls: t.Type["PydanticModel"], provided_fields: t.Set[str]) -> t.Set[str]: 139 return provided_fields - cls.all_fields() 140 141 @classmethod 142 def all_fields(cls: t.Type["PydanticModel"]) -> t.Set[str]: 143 return cls._fields() 144 145 @classmethod 146 def all_field_infos(cls: t.Type["PydanticModel"]) -> t.Dict[str, FieldInfo]: 147 return cls.model_fields 148 149 @classmethod 150 def required_fields(cls: t.Type["PydanticModel"]) -> t.Set[str]: 151 return cls._fields(lambda field: field.is_required()) 152 153 @classmethod 154 def _fields( 155 cls: t.Type["PydanticModel"], 156 predicate: t.Callable[[t.Any], bool] = lambda _: True, 157 ) -> t.Set[str]: 158 return { 159 field_info.alias if field_info.alias else field_name 160 for field_name, field_info in cls.all_field_infos().items() 161 if predicate(field_info) 162 } 163 164 def __eq__(self, other: t.Any) -> bool: 165 if (PYDANTIC_MAJOR_VERSION, PYDANTIC_MINOR_VERSION) < (2, 6): 166 if isinstance(other, pydantic.BaseModel): 167 return self.dict() == other.dict() 168 return self.dict() == other 169 return super().__eq__(other) 170 171 def __hash__(self) -> int: 172 if (PYDANTIC_MAJOR_VERSION, PYDANTIC_MINOR_VERSION) < (2, 6): 173 obj = {k: v for k, v in self.__dict__.items() if k in self.all_field_infos()} 174 return hash(self.__class__) + hash(tuple(obj.values())) 175 176 from pydantic._internal._model_construction import make_hash_func # type: ignore 177 178 if self.__class__ not in PydanticModel._hash_func_mapping: 179 PydanticModel._hash_func_mapping[self.__class__] = make_hash_func(self.__class__) 180 181 return PydanticModel._hash_func_mapping[self.__class__](self) 182 183 def __str__(self) -> str: 184 args = [] 185 186 for k, info in self.all_field_infos().items(): 187 v = getattr(self, k) 188 189 if type(v) != type(info.default) or v != info.default: 190 args.append(f"{k}: {v}") 191 192 return f"{self.__class__.__name__}<{', '.join(args)}>" 193 194 def __repr__(self) -> str: 195 return str(self)
!!! abstract "Usage Documentation" Models
A base class for creating Pydantic models.
Attributes:
- __class_vars__: The names of the class variables defined on the model.
- __private_attributes__: Metadata about the private attributes of the model.
- __signature__: The synthesized
__init__[Signature][inspect.Signature] of the model. - __pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
- __pydantic_core_schema__: The core schema of the model.
- __pydantic_custom_init__: Whether the model has a custom
__init__function. - __pydantic_decorators__: Metadata containing the decorators defined on the model.
This replaces
Model.__validators__andModel.__root_validators__from Pydantic V1. - __pydantic_generic_metadata__: A dictionary containing metadata about generic Pydantic models.
The
originandargsitems map to the [__origin__][genericalias.__origin__] and [__args__][genericalias.__args__] attributes of [generic aliases][types-genericalias], and theparameteritem maps to the__parameter__attribute of generic classes. - __pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
- __pydantic_post_init__: The name of the post-init method for the model, if defined.
- __pydantic_root_model__: Whether the model is a [
RootModel][pydantic.root_model.RootModel]. - __pydantic_serializer__: The
pydantic-coreSchemaSerializerused to dump instances of the model. - __pydantic_validator__: The
pydantic-coreSchemaValidatorused to validate instances of the model. - __pydantic_fields__: A dictionary of field names and their corresponding [
FieldInfo][pydantic.fields.FieldInfo] objects. - __pydantic_computed_fields__: A dictionary of computed field names and their corresponding [
ComputedFieldInfo][pydantic.fields.ComputedFieldInfo] objects. - __pydantic_extra__: A dictionary containing extra values, if [
extra][pydantic.config.ConfigDict.extra] is set to'allow'. - __pydantic_fields_set__: The names of fields explicitly set during instantiation.
- __pydantic_private__: Values of private attributes set on the model instance.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
105 def json( 106 self, 107 **kwargs: t.Any, 108 ) -> str: 109 kwargs = {**DEFAULT_ARGS, **kwargs} 110 # Pydantic v2 doesn't support arbitrary arguments for json.dump(). 111 if kwargs.pop("sort_keys", False): 112 return json.dumps(super().model_dump(mode="json", **kwargs), sort_keys=True) 113 114 return super().model_dump_json(**kwargs)
Returns a copy of the model.
!!! warning "Deprecated"
This method is now deprecated; use model_copy instead.
If you need include or exclude, use:
python {test="skip" lint="skip"}
data = self.model_dump(include=include, exclude=exclude, round_trip=True)
data = {**data, **(update or {})}
copied = self.model_validate(data)
Arguments:
- include: Optional set or mapping specifying which fields to include in the copied model.
- exclude: Optional set or mapping specifying which fields to exclude in the copied model.
- update: Optional dictionary of field-value pairs to override field values in the copied model.
- deep: If True, the values of fields that are Pydantic models will be deep-copied.
Returns:
A copy of the model with included, excluded and updated fields as specified.
Inherited Members
- pydantic.main.BaseModel
- BaseModel
- model_fields
- model_computed_fields
- model_extra
- model_fields_set
- model_construct
- model_copy
- model_dump
- model_dump_json
- model_json_schema
- model_parametrized_name
- model_post_init
- model_rebuild
- model_validate
- model_validate_json
- model_validate_strings
- parse_file
- from_orm
- construct
- schema
- schema_json
- validate
- update_forward_refs
212def validate_expression(expression: E, dialect: str) -> E: 213 # this normalizes and quotes identifiers in the given expression according the specified dialect 214 # it also sets expression.meta["dialect"] so that when we serialize for state, the expression is serialized in the correct dialect 215 return _get_field(expression, {"dialect": dialect}) # type: ignore
226def positive_int_validator(v: t.Any) -> int: 227 if isinstance(v, exp.Expr) and v.is_int: 228 v = int(v.name) 229 if not isinstance(v, int): 230 raise ValueError(f"Invalid num {v}. Value must be an integer value") 231 if v <= 0: 232 raise ValueError(f"Invalid num {v}. Value must be a positive integer") 233 return v
307def list_of_fields_or_star_validator( 308 v: t.Any, values: t.Any 309) -> t.Union[exp.Star, t.List[exp.Expr]]: 310 expressions = _get_fields(v, values) 311 if len(expressions) == 1 and isinstance(expressions[0], exp.Star): 312 return t.cast(exp.Star, expressions[0]) 313 return t.cast(t.List[exp.Expr], expressions)
316def cron_validator(v: t.Any) -> str: 317 if isinstance(v, exp.Expr): 318 v = v.name 319 320 from croniter import CroniterBadCronError, croniter 321 322 if not isinstance(v, str): 323 raise ValueError(f"Invalid cron expression '{v}'. Value must be a string.") 324 325 try: 326 croniter(v) 327 except CroniterBadCronError: 328 raise ValueError(f"Invalid cron expression '{v}'") 329 return v
332def get_concrete_types_from_typehint(typehint: type[t.Any]) -> set[type[t.Any]]: 333 concrete_types = set() 334 unpacked = t.get_origin(typehint) 335 if unpacked is None: 336 if type(typehint) == type(type): 337 return {typehint} 338 elif unpacked is t.Union: 339 for item in t.get_args(typehint): 340 if str(item).startswith("typing."): 341 concrete_types |= get_concrete_types_from_typehint(item) 342 else: 343 concrete_types.add(item) 344 else: 345 concrete_types.add(unpacked) 346 347 return concrete_types