from dbt.contracts.util import Replaceable, Mergeable, list_str from dbt.contracts.connection import UserConfigContract, QueryComment from dbt.helper_types import NoValue from dbt.logger import GLOBAL_LOGGER as logger # noqa from dbt import tracking from dbt import ui from dbt.dataclass_schema import ( dbtClassMixin, ValidationError, HyphenatedDbtClassMixin, ExtensibleDbtClassMixin, register_pattern, ValidatedStringMixin ) from dataclasses import dataclass, field from typing import Optional, List, Dict, Union, Any from mashumaro.types import SerializableType PIN_PACKAGE_URL = 'https://docs.getdbt.com/docs/package-management#section-specifying-package-versions' # noqa DEFAULT_SEND_ANONYMOUS_USAGE_STATS = True class Name(ValidatedStringMixin): ValidationRegex = r'^[^\d\W]\w*$' register_pattern(Name, r'^[^\d\W]\w*$') class SemverString(str, SerializableType): def _serialize(self) -> str: return self @classmethod def _deserialize(cls, value: str) -> 'SemverString': return SemverString(value) # this does not support the full semver (does not allow a trailing -fooXYZ) and # is not restrictive enough for full semver, (allows '1.0'). But it's like # 'semver lite'. register_pattern( SemverString, r'^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(\.(?:0|[1-9]\d*))?$', ) @dataclass class Quoting(dbtClassMixin, Mergeable): schema: Optional[bool] = None database: Optional[bool] = None project: Optional[bool] = None identifier: Optional[bool] = None @dataclass class Package(Replaceable, HyphenatedDbtClassMixin): pass @dataclass class LocalPackage(Package): local: str # `float` also allows `int`, according to PEP484 (and jsonschema!) RawVersion = Union[str, float] @dataclass class GitPackage(Package): git: str revision: Optional[RawVersion] = None warn_unpinned: Optional[bool] = None subdirectory: Optional[str] = None def get_revisions(self) -> List[str]: if self.revision is None: return [] else: return [str(self.revision)] @dataclass class RegistryPackage(Package): package: str version: Union[RawVersion, List[RawVersion]] install_prerelease: Optional[bool] = False def get_versions(self) -> List[str]: if isinstance(self.version, list): return [str(v) for v in self.version] else: return [str(self.version)] PackageSpec = Union[LocalPackage, GitPackage, RegistryPackage] @dataclass class PackageConfig(dbtClassMixin, Replaceable): packages: List[PackageSpec] @dataclass class ProjectPackageMetadata: name: str packages: List[PackageSpec] @classmethod def from_project(cls, project): return cls(name=project.project_name, packages=project.packages.packages) @dataclass class Downloads(ExtensibleDbtClassMixin, Replaceable): tarball: str @dataclass class RegistryPackageMetadata( ExtensibleDbtClassMixin, ProjectPackageMetadata, ): downloads: Downloads # A list of all the reserved words that packages may not have as names. BANNED_PROJECT_NAMES = { '_sql_results', 'adapter', 'api', 'column', 'config', 'context', 'database', 'env', 'env_var', 'exceptions', 'execute', 'flags', 'fromjson', 'fromyaml', 'graph', 'invocation_id', 'load_agate_table', 'load_result', 'log', 'model', 'modules', 'post_hooks', 'pre_hooks', 'ref', 'render', 'return', 'run_started_at', 'schema', 'source', 'sql', 'sql_now', 'store_result', 'store_raw_result', 'target', 'this', 'tojson', 'toyaml', 'try_or_compiler_error', 'var', 'write', } @dataclass class Project(HyphenatedDbtClassMixin, Replaceable): name: Name version: Union[SemverString, float] config_version: int project_root: Optional[str] = None source_paths: Optional[List[str]] = None macro_paths: Optional[List[str]] = None data_paths: Optional[List[str]] = None test_paths: Optional[List[str]] = None analysis_paths: Optional[List[str]] = None docs_paths: Optional[List[str]] = None asset_paths: Optional[List[str]] = None target_path: Optional[str] = None snapshot_paths: Optional[List[str]] = None clean_targets: Optional[List[str]] = None profile: Optional[str] = None log_path: Optional[str] = None modules_path: Optional[str] = None quoting: Optional[Quoting] = None on_run_start: Optional[List[str]] = field(default_factory=list_str) on_run_end: Optional[List[str]] = field(default_factory=list_str) require_dbt_version: Optional[Union[List[str], str]] = None dispatch: List[Dict[str, Any]] = field(default_factory=list) models: Dict[str, Any] = field(default_factory=dict) seeds: Dict[str, Any] = field(default_factory=dict) snapshots: Dict[str, Any] = field(default_factory=dict) analyses: Dict[str, Any] = field(default_factory=dict) sources: Dict[str, Any] = field(default_factory=dict) tests: Dict[str, Any] = field(default_factory=dict) vars: Optional[Dict[str, Any]] = field( default=None, metadata=dict( description='map project names to their vars override dicts', ), ) packages: List[PackageSpec] = field(default_factory=list) query_comment: Optional[Union[QueryComment, NoValue, str]] = NoValue() @classmethod def validate(cls, data): super().validate(data) if data['name'] in BANNED_PROJECT_NAMES: raise ValidationError( f"Invalid project name: {data['name']} is a reserved word" ) # validate dispatch config if 'dispatch' in data and data['dispatch']: entries = data['dispatch'] for entry in entries: if ('macro_namespace' not in entry or 'search_order' not in entry or not isinstance(entry['search_order'], list)): raise ValidationError(f"Invalid project dispatch config: {entry}") @dataclass class UserConfig(ExtensibleDbtClassMixin, Replaceable, UserConfigContract): send_anonymous_usage_stats: bool = DEFAULT_SEND_ANONYMOUS_USAGE_STATS use_colors: Optional[bool] = None partial_parse: Optional[bool] = None printer_width: Optional[int] = None def set_values(self, cookie_dir): if self.send_anonymous_usage_stats: tracking.initialize_tracking(cookie_dir) else: tracking.do_not_track() if self.use_colors is not None: ui.use_colors(self.use_colors) if self.printer_width: ui.printer_width(self.printer_width) @dataclass class ProfileConfig(HyphenatedDbtClassMixin, Replaceable): profile_name: str = field(metadata={'preserve_underscore': True}) target_name: str = field(metadata={'preserve_underscore': True}) config: UserConfig threads: int # TODO: make this a dynamic union of some kind? credentials: Optional[Dict[str, Any]] @dataclass class ConfiguredQuoting(Quoting, Replaceable): identifier: bool = True schema: bool = True database: Optional[bool] = None project: Optional[bool] = None @dataclass class Configuration(Project, ProfileConfig): cli_vars: Dict[str, Any] = field( default_factory=dict, metadata={'preserve_underscore': True}, ) quoting: Optional[ConfiguredQuoting] = None @dataclass class ProjectList(dbtClassMixin): projects: Dict[str, Project]