sqlmesh.core.config.root
1from __future__ import annotations 2 3import pickle 4import re 5import typing as t 6import zlib 7 8from pydantic import Field 9from pydantic.functional_validators import BeforeValidator 10from sqlglot import exp 11from sqlglot.helper import first 12from sqlglot.optimizer.normalize_identifiers import normalize_identifiers 13 14from sqlmesh.cicd.config import CICDBotConfig 15from sqlmesh.core import constants as c 16from sqlmesh.core.console import get_console 17from sqlmesh.core.config.common import ( 18 EnvironmentSuffixTarget, 19 TableNamingConvention, 20 VirtualEnvironmentMode, 21) 22from sqlmesh.core.config.base import BaseConfig, UpdateStrategy 23from sqlmesh.core.config.common import variables_validator, compile_regex_mapping 24from sqlmesh.core.config.connection import ( 25 ConnectionConfig, 26 DuckDBConnectionConfig, 27 SerializableConnectionConfig, 28 connection_config_validator, 29) 30from sqlmesh.core.config.format import FormatConfig 31from sqlmesh.core.config.gateway import GatewayConfig 32from sqlmesh.core.config.janitor import JanitorConfig 33from sqlmesh.core.config.migration import MigrationConfig 34from sqlmesh.core.config.model import ModelDefaultsConfig 35from sqlmesh.core.config.naming import NameInferenceConfig as NameInferenceConfig 36from sqlmesh.core.config.linter import LinterConfig as LinterConfig 37from sqlmesh.core.config.plan import PlanConfig 38from sqlmesh.core.config.run import RunConfig 39from sqlmesh.core.config.dbt import DbtConfig 40from sqlmesh.core.config.scheduler import ( 41 BuiltInSchedulerConfig, 42 SchedulerConfig, 43 scheduler_config_validator, 44) 45from sqlmesh.core.config.ui import UIConfig 46from sqlmesh.core.loader import Loader, SqlMeshLoader 47from sqlmesh.core.notification_target import NotificationTarget 48from sqlmesh.core.user import User 49from sqlmesh.utils.date import to_timestamp, now 50from sqlmesh.utils.errors import ConfigError 51from sqlmesh.utils.pydantic import model_validator 52 53 54def validate_no_past_ttl(v: str) -> str: 55 current_time = now() 56 if to_timestamp(v, relative_base=current_time) < to_timestamp(current_time): 57 raise ValueError( 58 f"TTL '{v}' is in the past. Please specify a relative time in the future. Ex: `in 1 week` instead of `1 week`." 59 ) 60 return v 61 62 63def gateways_ensure_dict(value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: 64 try: 65 if not isinstance(value, GatewayConfig): 66 GatewayConfig.parse_obj(value) 67 return {"": value} 68 except Exception: 69 # Normalize all gateway keys to lowercase for case-insensitive matching 70 if isinstance(value, dict): 71 return {k.lower(): v for k, v in value.items()} 72 return value 73 74 75def validate_regex_key_dict(value: t.Dict[str | re.Pattern, t.Any]) -> t.Dict[re.Pattern, t.Any]: 76 return compile_regex_mapping(value) 77 78 79def _canonicalize(obj: object) -> object: 80 """Recursively convert an object into a canonical, order-stable form for hashing. 81 82 ``set``/``frozenset`` iteration order is not stable across Python processes, so 83 pickling them directly yields non-deterministic bytes. That makes any hash derived 84 from the pickle (e.g. ``Config.fingerprint``) change run-to-run, which silently 85 invalidates on-disk caches keyed by the fingerprint. Sorting set members into a 86 list restores determinism while preserving contents. 87 """ 88 if isinstance(obj, (set, frozenset)): 89 return sorted(map(_canonicalize, obj)) # type: ignore[type-var] 90 if isinstance(obj, dict): 91 return {k: _canonicalize(v) for k, v in obj.items()} 92 if isinstance(obj, (list, tuple)): 93 return type(obj)(map(_canonicalize, obj)) 94 return obj 95 96 97if t.TYPE_CHECKING: 98 from sqlmesh.core._typing import Self 99 100 NoPastTTLString = str 101 GatewayDict = t.Dict[str, GatewayConfig] 102 RegexKeyDict = t.Dict[re.Pattern, str] 103else: 104 NoPastTTLString = t.Annotated[str, BeforeValidator(validate_no_past_ttl)] 105 GatewayDict = t.Annotated[t.Dict[str, GatewayConfig], BeforeValidator(gateways_ensure_dict)] 106 RegexKeyDict = t.Annotated[t.Dict[re.Pattern, str], BeforeValidator(validate_regex_key_dict)] 107 108 109class Config(BaseConfig): 110 """An object used by a Context to configure your SQLMesh project. 111 112 Args: 113 gateways: Supported gateways and their configurations. Key represents a unique name of a gateway. 114 default_connection: The default connection to use if one is not specified in a gateway. 115 default_test_connection: The default connection to use for tests if one is not specified in a gateway. 116 default_scheduler: The default scheduler configuration to use if one is not specified in a gateway. 117 default_gateway: The default gateway. 118 notification_targets: The notification targets to use. 119 project: The project name of this config. Used for multi-repo setups. 120 snapshot_ttl: The period of time that a model snapshot that is not a part of any environment should exist before being deleted. 121 environment_ttl: The period of time that a development environment should exist before being deleted. 122 ignore_patterns: Files that match glob patterns specified in this list are ignored when scanning the project folder. 123 time_column_format: The default format to use for all model time columns. Defaults to %Y-%m-%d. 124 This time format uses python format codes. https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes. 125 users: A list of users that can be used for approvals/notifications. 126 username: Name of a single user who should receive approvals/notification, instead of all users in the `users` list. 127 pinned_environments: A list of development environment names that should not be deleted by the janitor task. 128 loader: Loader class used for loading project files. 129 loader_kwargs: Key-value arguments to pass to the loader instance. 130 env_vars: A dictionary of environmental variable names and values. 131 model_defaults: Default values for model definitions. 132 physical_schema_mapping: A mapping from regular expressions to names of schemas in which physical tables for corresponding models will be placed. 133 environment_suffix_target: Indicates whether to append the environment name to the schema or table name. 134 physical_table_naming_convention: Indicates how tables should be named at the physical layer 135 virtual_environment_mode: Indicates how environments should be handled. 136 gateway_managed_virtual_layer: Whether the models' views in the virtual layer are created by the model-specific gateway rather than the default gateway. 137 infer_python_dependencies: Whether to statically analyze Python code to automatically infer Python package requirements. 138 environment_catalog_mapping: A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment. 139 default_target_environment: The name of the environment that will be the default target for the `sqlmesh plan` and `sqlmesh run` commands. 140 log_limit: The default number of logs to keep. 141 format: The formatting options for SQL code. 142 ui: The UI configuration for SQLMesh. 143 plan: The plan configuration. 144 migration: The migration configuration. 145 variables: A dictionary of variables that can be used in models / macros. 146 disable_anonymized_analytics: Whether to disable the anonymized analytics collection. 147 before_all: SQL statements or macros to be executed at the start of the `sqlmesh plan` and `sqlmesh run` commands. 148 after_all: SQL statements or macros to be executed at the end of the `sqlmesh plan` and `sqlmesh run` commands. 149 cache_dir: The directory to store the SQLMesh cache. Defaults to .cache in the project folder. 150 """ 151 152 gateways: GatewayDict = {"": GatewayConfig()} 153 default_connection: t.Optional[SerializableConnectionConfig] = None 154 default_test_connection_: t.Optional[SerializableConnectionConfig] = Field( 155 default=None, alias="default_test_connection" 156 ) 157 default_scheduler: SchedulerConfig = BuiltInSchedulerConfig() 158 default_gateway: str = "" 159 notification_targets: t.List[NotificationTarget] = [] 160 project: str = "" 161 snapshot_ttl: NoPastTTLString = c.DEFAULT_SNAPSHOT_TTL 162 environment_ttl: t.Optional[NoPastTTLString] = c.DEFAULT_ENVIRONMENT_TTL 163 ignore_patterns: t.List[str] = c.IGNORE_PATTERNS 164 time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT 165 users: t.List[User] = [] 166 model_defaults: ModelDefaultsConfig = ModelDefaultsConfig() 167 pinned_environments: t.Set[str] = set() 168 loader: t.Type[Loader] = SqlMeshLoader 169 loader_kwargs: t.Dict[str, t.Any] = {} 170 env_vars: t.Dict[str, str] = {} 171 username: str = "" 172 physical_schema_mapping: RegexKeyDict = {} 173 environment_suffix_target: EnvironmentSuffixTarget = EnvironmentSuffixTarget.default 174 physical_table_naming_convention: TableNamingConvention = TableNamingConvention.default 175 virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default 176 gateway_managed_virtual_layer: bool = False 177 infer_python_dependencies: bool = True 178 environment_catalog_mapping: RegexKeyDict = {} 179 default_target_environment: str = c.PROD 180 log_limit: int = c.DEFAULT_LOG_LIMIT 181 cicd_bot: t.Optional[CICDBotConfig] = None 182 run: RunConfig = RunConfig() 183 format: FormatConfig = FormatConfig() 184 ui: UIConfig = UIConfig() 185 plan: PlanConfig = PlanConfig() 186 migration: MigrationConfig = MigrationConfig() 187 model_naming: NameInferenceConfig = NameInferenceConfig() 188 variables: t.Dict[str, t.Any] = {} 189 disable_anonymized_analytics: bool = False 190 before_all: t.Optional[t.List[str]] = None 191 after_all: t.Optional[t.List[str]] = None 192 linter: LinterConfig = LinterConfig() 193 janitor: JanitorConfig = JanitorConfig() 194 cache_dir: t.Optional[str] = None 195 dbt: t.Optional[DbtConfig] = None 196 197 _FIELD_UPDATE_STRATEGY: t.ClassVar[t.Dict[str, UpdateStrategy]] = { 198 "gateways": UpdateStrategy.NESTED_UPDATE, 199 "notification_targets": UpdateStrategy.EXTEND, 200 "ignore_patterns": UpdateStrategy.EXTEND, 201 "users": UpdateStrategy.EXTEND, 202 "model_defaults": UpdateStrategy.NESTED_UPDATE, 203 "auto_categorize_changes": UpdateStrategy.NESTED_UPDATE, 204 "pinned_environments": UpdateStrategy.EXTEND, 205 "physical_schema_override": UpdateStrategy.KEY_UPDATE, 206 "run": UpdateStrategy.NESTED_UPDATE, 207 "format": UpdateStrategy.NESTED_UPDATE, 208 "ui": UpdateStrategy.NESTED_UPDATE, 209 "loader_kwargs": UpdateStrategy.KEY_UPDATE, 210 "plan": UpdateStrategy.NESTED_UPDATE, 211 "before_all": UpdateStrategy.EXTEND, 212 "after_all": UpdateStrategy.EXTEND, 213 "linter": UpdateStrategy.NESTED_UPDATE, 214 "dbt": UpdateStrategy.NESTED_UPDATE, 215 } 216 217 _connection_config_validator = connection_config_validator 218 _scheduler_config_validator = scheduler_config_validator # type: ignore 219 _variables_validator = variables_validator 220 221 @model_validator(mode="before") 222 def _normalize_and_validate_fields(cls, data: t.Any) -> t.Any: 223 if not isinstance(data, dict): 224 return data 225 226 if "gateways" not in data and "gateway" in data: 227 data["gateways"] = data.pop("gateway") 228 229 for plan_deprecated in ("auto_categorize_changes", "include_unmodified"): 230 if plan_deprecated in data: 231 raise ConfigError( 232 f"The `{plan_deprecated}` config is deprecated. Please use the `plan.{plan_deprecated}` config instead." 233 ) 234 235 if "physical_schema_override" in data: 236 get_console().log_warning( 237 "`physical_schema_override` is deprecated. Please use `physical_schema_mapping` instead." 238 ) 239 240 if "physical_schema_mapping" in data: 241 raise ConfigError( 242 "Only one of `physical_schema_override` and `physical_schema_mapping` can be specified." 243 ) 244 245 physical_schema_override: t.Dict[str, str] = data.pop("physical_schema_override") 246 # translate physical_schema_override to physical_schema_mapping 247 data["physical_schema_mapping"] = { 248 f"^{k}$": v for k, v in physical_schema_override.items() 249 } 250 251 return data 252 253 @model_validator(mode="after") 254 def _normalize_fields_after(self) -> Self: 255 dialect = self.model_defaults.dialect 256 257 def _normalize_identifiers(key: str) -> None: 258 setattr( 259 self, 260 key, 261 { 262 k: normalize_identifiers(v, dialect=dialect).name 263 for k, v in getattr(self, key, {}).items() 264 }, 265 ) 266 267 if ( 268 self.environment_suffix_target == EnvironmentSuffixTarget.CATALOG 269 and self.environment_catalog_mapping 270 ): 271 raise ConfigError( 272 f"'environment_suffix_target: catalog' is mutually exclusive with 'environment_catalog_mapping'.\n" 273 "Please specify one or the other" 274 ) 275 276 if self.plan.use_finalized_state and not self.virtual_environment_mode.is_full: 277 raise ConfigError( 278 "Using the finalized state is only supported when `virtual_environment_mode` is set to `full`." 279 ) 280 281 if self.environment_catalog_mapping: 282 _normalize_identifiers("environment_catalog_mapping") 283 if self.physical_schema_mapping: 284 _normalize_identifiers("physical_schema_mapping") 285 286 return self 287 288 @model_validator(mode="after") 289 def _inherit_project_config_in_cicd_bot(self) -> Self: 290 if self.cicd_bot: 291 # inherit the project-level settings into the CICD bot if they have not been explicitly overridden 292 if self.cicd_bot.auto_categorize_changes_ is None: 293 self.cicd_bot.auto_categorize_changes_ = self.plan.auto_categorize_changes 294 295 if self.cicd_bot.pr_include_unmodified_ is None: 296 self.cicd_bot.pr_include_unmodified_ = self.plan.include_unmodified 297 298 return self 299 300 def get_default_test_connection( 301 self, 302 default_catalog: t.Optional[str] = None, 303 default_catalog_dialect: t.Optional[str] = None, 304 ) -> ConnectionConfig: 305 return self.default_test_connection_ or DuckDBConnectionConfig( 306 catalogs=( 307 None 308 if default_catalog is None 309 else { 310 # transpile catalog name from main connection dialect to DuckDB 311 exp.parse_identifier(default_catalog, dialect=default_catalog_dialect).sql( 312 dialect="duckdb" 313 ): ":memory:" 314 } 315 ) 316 ) 317 318 def get_gateway(self, name: t.Optional[str] = None) -> GatewayConfig: 319 if isinstance(self.gateways, dict): 320 if name is None: 321 if self.default_gateway: 322 # Normalize default_gateway name to lowercase for lookup 323 default_key = self.default_gateway.lower() 324 if default_key not in self.gateways: 325 raise ConfigError(f"Missing gateway with name '{self.default_gateway}'") 326 return self.gateways[default_key] 327 328 if "" in self.gateways: 329 return self.gateways[""] 330 331 return first(self.gateways.values()) 332 333 # Normalize lookup name to lowercase since gateway keys are already lowercase 334 lookup_key = name.lower() 335 if lookup_key not in self.gateways: 336 raise ConfigError(f"Missing gateway with name '{name}'.") 337 338 return self.gateways[lookup_key] 339 if name is not None: 340 raise ConfigError("Gateway name is not supported when only one gateway is configured.") 341 return self.gateways 342 343 def get_connection(self, gateway_name: t.Optional[str] = None) -> ConnectionConfig: 344 connection = self.get_gateway(gateway_name).connection or self.default_connection 345 if connection is None: 346 msg = f" for gateway '{gateway_name}'" if gateway_name else "" 347 raise ConfigError(f"No connection configured{msg}.") 348 return connection 349 350 def get_state_connection( 351 self, gateway_name: t.Optional[str] = None 352 ) -> t.Optional[ConnectionConfig]: 353 return self.get_gateway(gateway_name).state_connection 354 355 def get_test_connection( 356 self, 357 gateway_name: t.Optional[str] = None, 358 default_catalog: t.Optional[str] = None, 359 default_catalog_dialect: t.Optional[str] = None, 360 ) -> ConnectionConfig: 361 return self.get_gateway(gateway_name).test_connection or self.get_default_test_connection( 362 default_catalog=default_catalog, default_catalog_dialect=default_catalog_dialect 363 ) 364 365 def get_scheduler(self, gateway_name: t.Optional[str] = None) -> SchedulerConfig: 366 return self.get_gateway(gateway_name).scheduler or self.default_scheduler 367 368 def get_state_schema(self, gateway_name: t.Optional[str] = None) -> t.Optional[str]: 369 return self.get_gateway(gateway_name).state_schema 370 371 @property 372 def default_gateway_name(self) -> str: 373 if self.default_gateway: 374 return self.default_gateway 375 if "" in self.gateways: 376 return "" 377 return first(self.gateways) 378 379 @property 380 def dialect(self) -> t.Optional[str]: 381 return self.model_defaults.dialect 382 383 @property 384 def fingerprint(self) -> str: 385 return str( 386 zlib.crc32( 387 pickle.dumps(_canonicalize(self.dict(exclude={"loader", "notification_targets"}))) 388 ) 389 )
def
validate_no_past_ttl(v: str) -> str:
def
gateways_ensure_dict(value: Dict[str, Any]) -> Dict[str, Any]:
64def gateways_ensure_dict(value: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: 65 try: 66 if not isinstance(value, GatewayConfig): 67 GatewayConfig.parse_obj(value) 68 return {"": value} 69 except Exception: 70 # Normalize all gateway keys to lowercase for case-insensitive matching 71 if isinstance(value, dict): 72 return {k.lower(): v for k, v in value.items()} 73 return value
def
validate_regex_key_dict(value: Dict[str | re.Pattern, Any]) -> Dict[re.Pattern, Any]:
110class Config(BaseConfig): 111 """An object used by a Context to configure your SQLMesh project. 112 113 Args: 114 gateways: Supported gateways and their configurations. Key represents a unique name of a gateway. 115 default_connection: The default connection to use if one is not specified in a gateway. 116 default_test_connection: The default connection to use for tests if one is not specified in a gateway. 117 default_scheduler: The default scheduler configuration to use if one is not specified in a gateway. 118 default_gateway: The default gateway. 119 notification_targets: The notification targets to use. 120 project: The project name of this config. Used for multi-repo setups. 121 snapshot_ttl: The period of time that a model snapshot that is not a part of any environment should exist before being deleted. 122 environment_ttl: The period of time that a development environment should exist before being deleted. 123 ignore_patterns: Files that match glob patterns specified in this list are ignored when scanning the project folder. 124 time_column_format: The default format to use for all model time columns. Defaults to %Y-%m-%d. 125 This time format uses python format codes. https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes. 126 users: A list of users that can be used for approvals/notifications. 127 username: Name of a single user who should receive approvals/notification, instead of all users in the `users` list. 128 pinned_environments: A list of development environment names that should not be deleted by the janitor task. 129 loader: Loader class used for loading project files. 130 loader_kwargs: Key-value arguments to pass to the loader instance. 131 env_vars: A dictionary of environmental variable names and values. 132 model_defaults: Default values for model definitions. 133 physical_schema_mapping: A mapping from regular expressions to names of schemas in which physical tables for corresponding models will be placed. 134 environment_suffix_target: Indicates whether to append the environment name to the schema or table name. 135 physical_table_naming_convention: Indicates how tables should be named at the physical layer 136 virtual_environment_mode: Indicates how environments should be handled. 137 gateway_managed_virtual_layer: Whether the models' views in the virtual layer are created by the model-specific gateway rather than the default gateway. 138 infer_python_dependencies: Whether to statically analyze Python code to automatically infer Python package requirements. 139 environment_catalog_mapping: A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment. 140 default_target_environment: The name of the environment that will be the default target for the `sqlmesh plan` and `sqlmesh run` commands. 141 log_limit: The default number of logs to keep. 142 format: The formatting options for SQL code. 143 ui: The UI configuration for SQLMesh. 144 plan: The plan configuration. 145 migration: The migration configuration. 146 variables: A dictionary of variables that can be used in models / macros. 147 disable_anonymized_analytics: Whether to disable the anonymized analytics collection. 148 before_all: SQL statements or macros to be executed at the start of the `sqlmesh plan` and `sqlmesh run` commands. 149 after_all: SQL statements or macros to be executed at the end of the `sqlmesh plan` and `sqlmesh run` commands. 150 cache_dir: The directory to store the SQLMesh cache. Defaults to .cache in the project folder. 151 """ 152 153 gateways: GatewayDict = {"": GatewayConfig()} 154 default_connection: t.Optional[SerializableConnectionConfig] = None 155 default_test_connection_: t.Optional[SerializableConnectionConfig] = Field( 156 default=None, alias="default_test_connection" 157 ) 158 default_scheduler: SchedulerConfig = BuiltInSchedulerConfig() 159 default_gateway: str = "" 160 notification_targets: t.List[NotificationTarget] = [] 161 project: str = "" 162 snapshot_ttl: NoPastTTLString = c.DEFAULT_SNAPSHOT_TTL 163 environment_ttl: t.Optional[NoPastTTLString] = c.DEFAULT_ENVIRONMENT_TTL 164 ignore_patterns: t.List[str] = c.IGNORE_PATTERNS 165 time_column_format: str = c.DEFAULT_TIME_COLUMN_FORMAT 166 users: t.List[User] = [] 167 model_defaults: ModelDefaultsConfig = ModelDefaultsConfig() 168 pinned_environments: t.Set[str] = set() 169 loader: t.Type[Loader] = SqlMeshLoader 170 loader_kwargs: t.Dict[str, t.Any] = {} 171 env_vars: t.Dict[str, str] = {} 172 username: str = "" 173 physical_schema_mapping: RegexKeyDict = {} 174 environment_suffix_target: EnvironmentSuffixTarget = EnvironmentSuffixTarget.default 175 physical_table_naming_convention: TableNamingConvention = TableNamingConvention.default 176 virtual_environment_mode: VirtualEnvironmentMode = VirtualEnvironmentMode.default 177 gateway_managed_virtual_layer: bool = False 178 infer_python_dependencies: bool = True 179 environment_catalog_mapping: RegexKeyDict = {} 180 default_target_environment: str = c.PROD 181 log_limit: int = c.DEFAULT_LOG_LIMIT 182 cicd_bot: t.Optional[CICDBotConfig] = None 183 run: RunConfig = RunConfig() 184 format: FormatConfig = FormatConfig() 185 ui: UIConfig = UIConfig() 186 plan: PlanConfig = PlanConfig() 187 migration: MigrationConfig = MigrationConfig() 188 model_naming: NameInferenceConfig = NameInferenceConfig() 189 variables: t.Dict[str, t.Any] = {} 190 disable_anonymized_analytics: bool = False 191 before_all: t.Optional[t.List[str]] = None 192 after_all: t.Optional[t.List[str]] = None 193 linter: LinterConfig = LinterConfig() 194 janitor: JanitorConfig = JanitorConfig() 195 cache_dir: t.Optional[str] = None 196 dbt: t.Optional[DbtConfig] = None 197 198 _FIELD_UPDATE_STRATEGY: t.ClassVar[t.Dict[str, UpdateStrategy]] = { 199 "gateways": UpdateStrategy.NESTED_UPDATE, 200 "notification_targets": UpdateStrategy.EXTEND, 201 "ignore_patterns": UpdateStrategy.EXTEND, 202 "users": UpdateStrategy.EXTEND, 203 "model_defaults": UpdateStrategy.NESTED_UPDATE, 204 "auto_categorize_changes": UpdateStrategy.NESTED_UPDATE, 205 "pinned_environments": UpdateStrategy.EXTEND, 206 "physical_schema_override": UpdateStrategy.KEY_UPDATE, 207 "run": UpdateStrategy.NESTED_UPDATE, 208 "format": UpdateStrategy.NESTED_UPDATE, 209 "ui": UpdateStrategy.NESTED_UPDATE, 210 "loader_kwargs": UpdateStrategy.KEY_UPDATE, 211 "plan": UpdateStrategy.NESTED_UPDATE, 212 "before_all": UpdateStrategy.EXTEND, 213 "after_all": UpdateStrategy.EXTEND, 214 "linter": UpdateStrategy.NESTED_UPDATE, 215 "dbt": UpdateStrategy.NESTED_UPDATE, 216 } 217 218 _connection_config_validator = connection_config_validator 219 _scheduler_config_validator = scheduler_config_validator # type: ignore 220 _variables_validator = variables_validator 221 222 @model_validator(mode="before") 223 def _normalize_and_validate_fields(cls, data: t.Any) -> t.Any: 224 if not isinstance(data, dict): 225 return data 226 227 if "gateways" not in data and "gateway" in data: 228 data["gateways"] = data.pop("gateway") 229 230 for plan_deprecated in ("auto_categorize_changes", "include_unmodified"): 231 if plan_deprecated in data: 232 raise ConfigError( 233 f"The `{plan_deprecated}` config is deprecated. Please use the `plan.{plan_deprecated}` config instead." 234 ) 235 236 if "physical_schema_override" in data: 237 get_console().log_warning( 238 "`physical_schema_override` is deprecated. Please use `physical_schema_mapping` instead." 239 ) 240 241 if "physical_schema_mapping" in data: 242 raise ConfigError( 243 "Only one of `physical_schema_override` and `physical_schema_mapping` can be specified." 244 ) 245 246 physical_schema_override: t.Dict[str, str] = data.pop("physical_schema_override") 247 # translate physical_schema_override to physical_schema_mapping 248 data["physical_schema_mapping"] = { 249 f"^{k}$": v for k, v in physical_schema_override.items() 250 } 251 252 return data 253 254 @model_validator(mode="after") 255 def _normalize_fields_after(self) -> Self: 256 dialect = self.model_defaults.dialect 257 258 def _normalize_identifiers(key: str) -> None: 259 setattr( 260 self, 261 key, 262 { 263 k: normalize_identifiers(v, dialect=dialect).name 264 for k, v in getattr(self, key, {}).items() 265 }, 266 ) 267 268 if ( 269 self.environment_suffix_target == EnvironmentSuffixTarget.CATALOG 270 and self.environment_catalog_mapping 271 ): 272 raise ConfigError( 273 f"'environment_suffix_target: catalog' is mutually exclusive with 'environment_catalog_mapping'.\n" 274 "Please specify one or the other" 275 ) 276 277 if self.plan.use_finalized_state and not self.virtual_environment_mode.is_full: 278 raise ConfigError( 279 "Using the finalized state is only supported when `virtual_environment_mode` is set to `full`." 280 ) 281 282 if self.environment_catalog_mapping: 283 _normalize_identifiers("environment_catalog_mapping") 284 if self.physical_schema_mapping: 285 _normalize_identifiers("physical_schema_mapping") 286 287 return self 288 289 @model_validator(mode="after") 290 def _inherit_project_config_in_cicd_bot(self) -> Self: 291 if self.cicd_bot: 292 # inherit the project-level settings into the CICD bot if they have not been explicitly overridden 293 if self.cicd_bot.auto_categorize_changes_ is None: 294 self.cicd_bot.auto_categorize_changes_ = self.plan.auto_categorize_changes 295 296 if self.cicd_bot.pr_include_unmodified_ is None: 297 self.cicd_bot.pr_include_unmodified_ = self.plan.include_unmodified 298 299 return self 300 301 def get_default_test_connection( 302 self, 303 default_catalog: t.Optional[str] = None, 304 default_catalog_dialect: t.Optional[str] = None, 305 ) -> ConnectionConfig: 306 return self.default_test_connection_ or DuckDBConnectionConfig( 307 catalogs=( 308 None 309 if default_catalog is None 310 else { 311 # transpile catalog name from main connection dialect to DuckDB 312 exp.parse_identifier(default_catalog, dialect=default_catalog_dialect).sql( 313 dialect="duckdb" 314 ): ":memory:" 315 } 316 ) 317 ) 318 319 def get_gateway(self, name: t.Optional[str] = None) -> GatewayConfig: 320 if isinstance(self.gateways, dict): 321 if name is None: 322 if self.default_gateway: 323 # Normalize default_gateway name to lowercase for lookup 324 default_key = self.default_gateway.lower() 325 if default_key not in self.gateways: 326 raise ConfigError(f"Missing gateway with name '{self.default_gateway}'") 327 return self.gateways[default_key] 328 329 if "" in self.gateways: 330 return self.gateways[""] 331 332 return first(self.gateways.values()) 333 334 # Normalize lookup name to lowercase since gateway keys are already lowercase 335 lookup_key = name.lower() 336 if lookup_key not in self.gateways: 337 raise ConfigError(f"Missing gateway with name '{name}'.") 338 339 return self.gateways[lookup_key] 340 if name is not None: 341 raise ConfigError("Gateway name is not supported when only one gateway is configured.") 342 return self.gateways 343 344 def get_connection(self, gateway_name: t.Optional[str] = None) -> ConnectionConfig: 345 connection = self.get_gateway(gateway_name).connection or self.default_connection 346 if connection is None: 347 msg = f" for gateway '{gateway_name}'" if gateway_name else "" 348 raise ConfigError(f"No connection configured{msg}.") 349 return connection 350 351 def get_state_connection( 352 self, gateway_name: t.Optional[str] = None 353 ) -> t.Optional[ConnectionConfig]: 354 return self.get_gateway(gateway_name).state_connection 355 356 def get_test_connection( 357 self, 358 gateway_name: t.Optional[str] = None, 359 default_catalog: t.Optional[str] = None, 360 default_catalog_dialect: t.Optional[str] = None, 361 ) -> ConnectionConfig: 362 return self.get_gateway(gateway_name).test_connection or self.get_default_test_connection( 363 default_catalog=default_catalog, default_catalog_dialect=default_catalog_dialect 364 ) 365 366 def get_scheduler(self, gateway_name: t.Optional[str] = None) -> SchedulerConfig: 367 return self.get_gateway(gateway_name).scheduler or self.default_scheduler 368 369 def get_state_schema(self, gateway_name: t.Optional[str] = None) -> t.Optional[str]: 370 return self.get_gateway(gateway_name).state_schema 371 372 @property 373 def default_gateway_name(self) -> str: 374 if self.default_gateway: 375 return self.default_gateway 376 if "" in self.gateways: 377 return "" 378 return first(self.gateways) 379 380 @property 381 def dialect(self) -> t.Optional[str]: 382 return self.model_defaults.dialect 383 384 @property 385 def fingerprint(self) -> str: 386 return str( 387 zlib.crc32( 388 pickle.dumps(_canonicalize(self.dict(exclude={"loader", "notification_targets"}))) 389 ) 390 )
An object used by a Context to configure your SQLMesh project.
Arguments:
- gateways: Supported gateways and their configurations. Key represents a unique name of a gateway.
- default_connection: The default connection to use if one is not specified in a gateway.
- default_test_connection: The default connection to use for tests if one is not specified in a gateway.
- default_scheduler: The default scheduler configuration to use if one is not specified in a gateway.
- default_gateway: The default gateway.
- notification_targets: The notification targets to use.
- project: The project name of this config. Used for multi-repo setups.
- snapshot_ttl: The period of time that a model snapshot that is not a part of any environment should exist before being deleted.
- environment_ttl: The period of time that a development environment should exist before being deleted.
- ignore_patterns: Files that match glob patterns specified in this list are ignored when scanning the project folder.
- time_column_format: The default format to use for all model time columns. Defaults to %Y-%m-%d. This time format uses python format codes. https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes.
- users: A list of users that can be used for approvals/notifications.
- username: Name of a single user who should receive approvals/notification, instead of all users in the
userslist. - pinned_environments: A list of development environment names that should not be deleted by the janitor task.
- loader: Loader class used for loading project files.
- loader_kwargs: Key-value arguments to pass to the loader instance.
- env_vars: A dictionary of environmental variable names and values.
- model_defaults: Default values for model definitions.
- physical_schema_mapping: A mapping from regular expressions to names of schemas in which physical tables for corresponding models will be placed.
- environment_suffix_target: Indicates whether to append the environment name to the schema or table name.
- physical_table_naming_convention: Indicates how tables should be named at the physical layer
- virtual_environment_mode: Indicates how environments should be handled.
- gateway_managed_virtual_layer: Whether the models' views in the virtual layer are created by the model-specific gateway rather than the default gateway.
- infer_python_dependencies: Whether to statically analyze Python code to automatically infer Python package requirements.
- environment_catalog_mapping: A mapping from regular expressions to catalog names. The catalog name is used to determine the target catalog for a given environment.
- default_target_environment: The name of the environment that will be the default target for the
sqlmesh planandsqlmesh runcommands. - log_limit: The default number of logs to keep.
- format: The formatting options for SQL code.
- ui: The UI configuration for SQLMesh.
- plan: The plan configuration.
- migration: The migration configuration.
- variables: A dictionary of variables that can be used in models / macros.
- disable_anonymized_analytics: Whether to disable the anonymized analytics collection.
- before_all: SQL statements or macros to be executed at the start of the
sqlmesh planandsqlmesh runcommands. - after_all: SQL statements or macros to be executed at the end of the
sqlmesh planandsqlmesh runcommands. - cache_dir: The directory to store the SQLMesh cache. Defaults to .cache in the project folder.
gateways: Annotated[Dict[str, sqlmesh.core.config.gateway.GatewayConfig], BeforeValidator(func=<function gateways_ensure_dict at 0x7a2abec8eb90>, json_schema_input_type=PydanticUndefined)]
default_connection: Optional[Annotated[sqlmesh.core.config.connection.ConnectionConfig, SerializeAsAny()]]
default_test_connection_: Optional[Annotated[sqlmesh.core.config.connection.ConnectionConfig, SerializeAsAny()]]
default_scheduler: sqlmesh.core.config.scheduler.SchedulerConfig
notification_targets: List[Annotated[Union[sqlmesh.core.notification_target.BasicSMTPNotificationTarget, sqlmesh.core.notification_target.GenericNotificationTarget, sqlmesh.core.notification_target.ConsoleNotificationTarget, sqlmesh.core.notification_target.SlackApiNotificationTarget, sqlmesh.core.notification_target.SlackWebhookNotificationTarget], FieldInfo(annotation=NoneType, required=True, discriminator='type_')]]
snapshot_ttl: typing.Annotated[str, BeforeValidator(func=<function validate_no_past_ttl at 0x7a2abec4ad40>, json_schema_input_type=PydanticUndefined)]
environment_ttl: Optional[Annotated[str, BeforeValidator(func=<function validate_no_past_ttl at 0x7a2abec4ad40>, json_schema_input_type=PydanticUndefined)]]
users: List[sqlmesh.core.user.User]
model_defaults: sqlmesh.core.config.model.ModelDefaultsConfig
loader: Type[sqlmesh.core.loader.Loader]
physical_schema_mapping: Annotated[Dict[re.Pattern, str], BeforeValidator(func=<function validate_regex_key_dict at 0x7a2abecac1f0>, json_schema_input_type=PydanticUndefined)]
environment_suffix_target: sqlmesh.core.config.common.EnvironmentSuffixTarget
physical_table_naming_convention: sqlmesh.core.config.common.TableNamingConvention
virtual_environment_mode: sqlmesh.core.config.common.VirtualEnvironmentMode
environment_catalog_mapping: Annotated[Dict[re.Pattern, str], BeforeValidator(func=<function validate_regex_key_dict at 0x7a2abecac1f0>, json_schema_input_type=PydanticUndefined)]
cicd_bot: Optional[sqlmesh.integrations.github.cicd.config.GithubCICDBotConfig]
model_naming: sqlmesh.core.config.naming.NameInferenceConfig
dbt: Optional[sqlmesh.core.config.dbt.DbtConfig]
def
get_default_test_connection( self, default_catalog: Optional[str] = None, default_catalog_dialect: Optional[str] = None) -> sqlmesh.core.config.connection.ConnectionConfig:
301 def get_default_test_connection( 302 self, 303 default_catalog: t.Optional[str] = None, 304 default_catalog_dialect: t.Optional[str] = None, 305 ) -> ConnectionConfig: 306 return self.default_test_connection_ or DuckDBConnectionConfig( 307 catalogs=( 308 None 309 if default_catalog is None 310 else { 311 # transpile catalog name from main connection dialect to DuckDB 312 exp.parse_identifier(default_catalog, dialect=default_catalog_dialect).sql( 313 dialect="duckdb" 314 ): ":memory:" 315 } 316 ) 317 )
319 def get_gateway(self, name: t.Optional[str] = None) -> GatewayConfig: 320 if isinstance(self.gateways, dict): 321 if name is None: 322 if self.default_gateway: 323 # Normalize default_gateway name to lowercase for lookup 324 default_key = self.default_gateway.lower() 325 if default_key not in self.gateways: 326 raise ConfigError(f"Missing gateway with name '{self.default_gateway}'") 327 return self.gateways[default_key] 328 329 if "" in self.gateways: 330 return self.gateways[""] 331 332 return first(self.gateways.values()) 333 334 # Normalize lookup name to lowercase since gateway keys are already lowercase 335 lookup_key = name.lower() 336 if lookup_key not in self.gateways: 337 raise ConfigError(f"Missing gateway with name '{name}'.") 338 339 return self.gateways[lookup_key] 340 if name is not None: 341 raise ConfigError("Gateway name is not supported when only one gateway is configured.") 342 return self.gateways
def
get_connection( self, gateway_name: Optional[str] = None) -> sqlmesh.core.config.connection.ConnectionConfig:
344 def get_connection(self, gateway_name: t.Optional[str] = None) -> ConnectionConfig: 345 connection = self.get_gateway(gateway_name).connection or self.default_connection 346 if connection is None: 347 msg = f" for gateway '{gateway_name}'" if gateway_name else "" 348 raise ConfigError(f"No connection configured{msg}.") 349 return connection
def
get_state_connection( self, gateway_name: Optional[str] = None) -> Optional[sqlmesh.core.config.connection.ConnectionConfig]:
def
get_test_connection( self, gateway_name: Optional[str] = None, default_catalog: Optional[str] = None, default_catalog_dialect: Optional[str] = None) -> sqlmesh.core.config.connection.ConnectionConfig:
356 def get_test_connection( 357 self, 358 gateway_name: t.Optional[str] = None, 359 default_catalog: t.Optional[str] = None, 360 default_catalog_dialect: t.Optional[str] = None, 361 ) -> ConnectionConfig: 362 return self.get_gateway(gateway_name).test_connection or self.get_default_test_connection( 363 default_catalog=default_catalog, default_catalog_dialect=default_catalog_dialect 364 )
def
get_scheduler( self, gateway_name: Optional[str] = None) -> sqlmesh.core.config.scheduler.SchedulerConfig:
model_config =
{'json_encoders': {<class 'sqlglot.expressions.core.Expr'>: <function _expression_encoder>, <class 'sqlglot.expressions.datatypes.DataType'>: <function _expression_encoder>, <class 'sqlglot.expressions.query.Tuple'>: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery]: <function _expression_encoder>, typing.Union[sqlglot.expressions.query.Query, sqlmesh.core.dialect.JinjaQuery, sqlmesh.core.dialect.MacroFunc]: <function _expression_encoder>, <class 'datetime.tzinfo'>: <function PydanticModel.<lambda>>}, 'arbitrary_types_allowed': True, 'extra': 'forbid', 'protected_namespaces': ()}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
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