dvt-core 1.11.0b4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dvt-core might be problematic. Click here for more details.
- dvt/__init__.py +7 -0
- dvt/_pydantic_shim.py +26 -0
- dvt/adapters/__init__.py +16 -0
- dvt/adapters/multi_adapter_manager.py +268 -0
- dvt/artifacts/__init__.py +0 -0
- dvt/artifacts/exceptions/__init__.py +1 -0
- dvt/artifacts/exceptions/schemas.py +31 -0
- dvt/artifacts/resources/__init__.py +116 -0
- dvt/artifacts/resources/base.py +68 -0
- dvt/artifacts/resources/types.py +93 -0
- dvt/artifacts/resources/v1/analysis.py +10 -0
- dvt/artifacts/resources/v1/catalog.py +23 -0
- dvt/artifacts/resources/v1/components.py +275 -0
- dvt/artifacts/resources/v1/config.py +282 -0
- dvt/artifacts/resources/v1/documentation.py +11 -0
- dvt/artifacts/resources/v1/exposure.py +52 -0
- dvt/artifacts/resources/v1/function.py +53 -0
- dvt/artifacts/resources/v1/generic_test.py +32 -0
- dvt/artifacts/resources/v1/group.py +22 -0
- dvt/artifacts/resources/v1/hook.py +11 -0
- dvt/artifacts/resources/v1/macro.py +30 -0
- dvt/artifacts/resources/v1/metric.py +173 -0
- dvt/artifacts/resources/v1/model.py +146 -0
- dvt/artifacts/resources/v1/owner.py +10 -0
- dvt/artifacts/resources/v1/saved_query.py +112 -0
- dvt/artifacts/resources/v1/seed.py +42 -0
- dvt/artifacts/resources/v1/semantic_layer_components.py +72 -0
- dvt/artifacts/resources/v1/semantic_model.py +315 -0
- dvt/artifacts/resources/v1/singular_test.py +14 -0
- dvt/artifacts/resources/v1/snapshot.py +92 -0
- dvt/artifacts/resources/v1/source_definition.py +85 -0
- dvt/artifacts/resources/v1/sql_operation.py +10 -0
- dvt/artifacts/resources/v1/unit_test_definition.py +78 -0
- dvt/artifacts/schemas/__init__.py +0 -0
- dvt/artifacts/schemas/base.py +191 -0
- dvt/artifacts/schemas/batch_results.py +24 -0
- dvt/artifacts/schemas/catalog/__init__.py +12 -0
- dvt/artifacts/schemas/catalog/v1/__init__.py +0 -0
- dvt/artifacts/schemas/catalog/v1/catalog.py +60 -0
- dvt/artifacts/schemas/freshness/__init__.py +1 -0
- dvt/artifacts/schemas/freshness/v3/__init__.py +0 -0
- dvt/artifacts/schemas/freshness/v3/freshness.py +159 -0
- dvt/artifacts/schemas/manifest/__init__.py +2 -0
- dvt/artifacts/schemas/manifest/v12/__init__.py +0 -0
- dvt/artifacts/schemas/manifest/v12/manifest.py +212 -0
- dvt/artifacts/schemas/results.py +148 -0
- dvt/artifacts/schemas/run/__init__.py +2 -0
- dvt/artifacts/schemas/run/v5/__init__.py +0 -0
- dvt/artifacts/schemas/run/v5/run.py +184 -0
- dvt/artifacts/schemas/upgrades/__init__.py +4 -0
- dvt/artifacts/schemas/upgrades/upgrade_manifest.py +174 -0
- dvt/artifacts/schemas/upgrades/upgrade_manifest_dbt_version.py +2 -0
- dvt/artifacts/utils/validation.py +153 -0
- dvt/cli/__init__.py +1 -0
- dvt/cli/context.py +16 -0
- dvt/cli/exceptions.py +56 -0
- dvt/cli/flags.py +558 -0
- dvt/cli/main.py +971 -0
- dvt/cli/option_types.py +121 -0
- dvt/cli/options.py +79 -0
- dvt/cli/params.py +803 -0
- dvt/cli/requires.py +478 -0
- dvt/cli/resolvers.py +32 -0
- dvt/cli/types.py +40 -0
- dvt/clients/__init__.py +0 -0
- dvt/clients/checked_load.py +82 -0
- dvt/clients/git.py +164 -0
- dvt/clients/jinja.py +206 -0
- dvt/clients/jinja_static.py +245 -0
- dvt/clients/registry.py +192 -0
- dvt/clients/yaml_helper.py +68 -0
- dvt/compilation.py +833 -0
- dvt/compute/__init__.py +26 -0
- dvt/compute/base.py +288 -0
- dvt/compute/engines/__init__.py +13 -0
- dvt/compute/engines/duckdb_engine.py +368 -0
- dvt/compute/engines/spark_engine.py +273 -0
- dvt/compute/query_analyzer.py +212 -0
- dvt/compute/router.py +483 -0
- dvt/config/__init__.py +4 -0
- dvt/config/catalogs.py +95 -0
- dvt/config/compute_config.py +406 -0
- dvt/config/profile.py +411 -0
- dvt/config/profiles_v2.py +464 -0
- dvt/config/project.py +893 -0
- dvt/config/renderer.py +232 -0
- dvt/config/runtime.py +491 -0
- dvt/config/selectors.py +209 -0
- dvt/config/utils.py +78 -0
- dvt/connectors/.gitignore +6 -0
- dvt/connectors/README.md +306 -0
- dvt/connectors/catalog.yml +217 -0
- dvt/connectors/download_connectors.py +300 -0
- dvt/constants.py +29 -0
- dvt/context/__init__.py +0 -0
- dvt/context/base.py +746 -0
- dvt/context/configured.py +136 -0
- dvt/context/context_config.py +350 -0
- dvt/context/docs.py +82 -0
- dvt/context/exceptions_jinja.py +179 -0
- dvt/context/macro_resolver.py +195 -0
- dvt/context/macros.py +171 -0
- dvt/context/manifest.py +73 -0
- dvt/context/providers.py +2198 -0
- dvt/context/query_header.py +14 -0
- dvt/context/secret.py +59 -0
- dvt/context/target.py +74 -0
- dvt/contracts/__init__.py +0 -0
- dvt/contracts/files.py +413 -0
- dvt/contracts/graph/__init__.py +0 -0
- dvt/contracts/graph/manifest.py +1904 -0
- dvt/contracts/graph/metrics.py +98 -0
- dvt/contracts/graph/model_config.py +71 -0
- dvt/contracts/graph/node_args.py +42 -0
- dvt/contracts/graph/nodes.py +1806 -0
- dvt/contracts/graph/semantic_manifest.py +233 -0
- dvt/contracts/graph/unparsed.py +812 -0
- dvt/contracts/project.py +417 -0
- dvt/contracts/results.py +53 -0
- dvt/contracts/selection.py +23 -0
- dvt/contracts/sql.py +86 -0
- dvt/contracts/state.py +69 -0
- dvt/contracts/util.py +46 -0
- dvt/deprecations.py +347 -0
- dvt/deps/__init__.py +0 -0
- dvt/deps/base.py +153 -0
- dvt/deps/git.py +196 -0
- dvt/deps/local.py +80 -0
- dvt/deps/registry.py +131 -0
- dvt/deps/resolver.py +149 -0
- dvt/deps/tarball.py +121 -0
- dvt/docs/source/_ext/dbt_click.py +118 -0
- dvt/docs/source/conf.py +32 -0
- dvt/env_vars.py +64 -0
- dvt/event_time/event_time.py +40 -0
- dvt/event_time/sample_window.py +60 -0
- dvt/events/__init__.py +16 -0
- dvt/events/base_types.py +37 -0
- dvt/events/core_types_pb2.py +2 -0
- dvt/events/logging.py +109 -0
- dvt/events/types.py +2534 -0
- dvt/exceptions.py +1487 -0
- dvt/flags.py +89 -0
- dvt/graph/__init__.py +11 -0
- dvt/graph/cli.py +248 -0
- dvt/graph/graph.py +172 -0
- dvt/graph/queue.py +213 -0
- dvt/graph/selector.py +375 -0
- dvt/graph/selector_methods.py +976 -0
- dvt/graph/selector_spec.py +223 -0
- dvt/graph/thread_pool.py +18 -0
- dvt/hooks.py +21 -0
- dvt/include/README.md +49 -0
- dvt/include/__init__.py +3 -0
- dvt/include/global_project.py +4 -0
- dvt/include/starter_project/.gitignore +4 -0
- dvt/include/starter_project/README.md +15 -0
- dvt/include/starter_project/__init__.py +3 -0
- dvt/include/starter_project/analyses/.gitkeep +0 -0
- dvt/include/starter_project/dvt_project.yml +36 -0
- dvt/include/starter_project/macros/.gitkeep +0 -0
- dvt/include/starter_project/models/example/my_first_dbt_model.sql +27 -0
- dvt/include/starter_project/models/example/my_second_dbt_model.sql +6 -0
- dvt/include/starter_project/models/example/schema.yml +21 -0
- dvt/include/starter_project/seeds/.gitkeep +0 -0
- dvt/include/starter_project/snapshots/.gitkeep +0 -0
- dvt/include/starter_project/tests/.gitkeep +0 -0
- dvt/internal_deprecations.py +27 -0
- dvt/jsonschemas/__init__.py +3 -0
- dvt/jsonschemas/jsonschemas.py +309 -0
- dvt/jsonschemas/project/0.0.110.json +4717 -0
- dvt/jsonschemas/project/0.0.85.json +2015 -0
- dvt/jsonschemas/resources/0.0.110.json +2636 -0
- dvt/jsonschemas/resources/0.0.85.json +2536 -0
- dvt/jsonschemas/resources/latest.json +6773 -0
- dvt/links.py +4 -0
- dvt/materializations/__init__.py +0 -0
- dvt/materializations/incremental/__init__.py +0 -0
- dvt/materializations/incremental/microbatch.py +235 -0
- dvt/mp_context.py +8 -0
- dvt/node_types.py +37 -0
- dvt/parser/__init__.py +23 -0
- dvt/parser/analysis.py +21 -0
- dvt/parser/base.py +549 -0
- dvt/parser/common.py +267 -0
- dvt/parser/docs.py +52 -0
- dvt/parser/fixtures.py +51 -0
- dvt/parser/functions.py +30 -0
- dvt/parser/generic_test.py +100 -0
- dvt/parser/generic_test_builders.py +334 -0
- dvt/parser/hooks.py +119 -0
- dvt/parser/macros.py +137 -0
- dvt/parser/manifest.py +2204 -0
- dvt/parser/models.py +574 -0
- dvt/parser/partial.py +1179 -0
- dvt/parser/read_files.py +445 -0
- dvt/parser/schema_generic_tests.py +423 -0
- dvt/parser/schema_renderer.py +111 -0
- dvt/parser/schema_yaml_readers.py +936 -0
- dvt/parser/schemas.py +1467 -0
- dvt/parser/search.py +149 -0
- dvt/parser/seeds.py +28 -0
- dvt/parser/singular_test.py +20 -0
- dvt/parser/snapshots.py +44 -0
- dvt/parser/sources.py +557 -0
- dvt/parser/sql.py +63 -0
- dvt/parser/unit_tests.py +622 -0
- dvt/plugins/__init__.py +20 -0
- dvt/plugins/contracts.py +10 -0
- dvt/plugins/exceptions.py +2 -0
- dvt/plugins/manager.py +164 -0
- dvt/plugins/manifest.py +21 -0
- dvt/profiler.py +20 -0
- dvt/py.typed +1 -0
- dvt/runners/__init__.py +2 -0
- dvt/runners/exposure_runner.py +7 -0
- dvt/runners/no_op_runner.py +46 -0
- dvt/runners/saved_query_runner.py +7 -0
- dvt/selected_resources.py +8 -0
- dvt/task/__init__.py +0 -0
- dvt/task/base.py +504 -0
- dvt/task/build.py +197 -0
- dvt/task/clean.py +57 -0
- dvt/task/clone.py +162 -0
- dvt/task/compile.py +151 -0
- dvt/task/compute.py +366 -0
- dvt/task/debug.py +650 -0
- dvt/task/deps.py +280 -0
- dvt/task/docs/__init__.py +3 -0
- dvt/task/docs/generate.py +408 -0
- dvt/task/docs/index.html +250 -0
- dvt/task/docs/serve.py +28 -0
- dvt/task/freshness.py +323 -0
- dvt/task/function.py +122 -0
- dvt/task/group_lookup.py +46 -0
- dvt/task/init.py +374 -0
- dvt/task/list.py +237 -0
- dvt/task/printer.py +176 -0
- dvt/task/profiles.py +256 -0
- dvt/task/retry.py +175 -0
- dvt/task/run.py +1146 -0
- dvt/task/run_operation.py +142 -0
- dvt/task/runnable.py +802 -0
- dvt/task/seed.py +104 -0
- dvt/task/show.py +150 -0
- dvt/task/snapshot.py +57 -0
- dvt/task/sql.py +111 -0
- dvt/task/test.py +464 -0
- dvt/tests/fixtures/__init__.py +1 -0
- dvt/tests/fixtures/project.py +620 -0
- dvt/tests/util.py +651 -0
- dvt/tracking.py +529 -0
- dvt/utils/__init__.py +3 -0
- dvt/utils/artifact_upload.py +151 -0
- dvt/utils/utils.py +408 -0
- dvt/version.py +249 -0
- dvt_core-1.11.0b4.dist-info/METADATA +252 -0
- dvt_core-1.11.0b4.dist-info/RECORD +261 -0
- dvt_core-1.11.0b4.dist-info/WHEEL +5 -0
- dvt_core-1.11.0b4.dist-info/entry_points.txt +2 -0
- dvt_core-1.11.0b4.dist-info/top_level.txt +1 -0
dvt/task/debug.py
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
# coding=utf-8
|
|
2
|
+
import importlib
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
from collections import namedtuple
|
|
7
|
+
from enum import Flag
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
import dvt.exceptions
|
|
12
|
+
from dvt.artifacts.schemas.results import RunStatus
|
|
13
|
+
from dvt.cli.flags import Flags
|
|
14
|
+
from dvt.clients.yaml_helper import load_yaml_text
|
|
15
|
+
from dvt.config import PartialProject, Profile, Project
|
|
16
|
+
from dvt.config.renderer import DbtProjectYamlRenderer, ProfileRenderer
|
|
17
|
+
from dvt.events.types import DebugCmdOut, DebugCmdResult, OpenCommand
|
|
18
|
+
from dvt.links import ProfileConfigDocs
|
|
19
|
+
from dvt.mp_context import get_mp_context
|
|
20
|
+
from dvt.task.base import BaseTask, get_nearest_project_dir
|
|
21
|
+
from dvt.version import get_installed_version
|
|
22
|
+
|
|
23
|
+
import dbt_common.clients.system
|
|
24
|
+
import dbt_common.exceptions
|
|
25
|
+
from dbt.adapters.factory import get_adapter, register_adapter
|
|
26
|
+
from dbt_common.events.format import pluralize
|
|
27
|
+
from dbt_common.events.functions import fire_event
|
|
28
|
+
from dbt_common.ui import green, red
|
|
29
|
+
|
|
30
|
+
ONLY_PROFILE_MESSAGE = """
|
|
31
|
+
A `dbt_project.yml` file was not found in this directory.
|
|
32
|
+
Using the only profile `{}`.
|
|
33
|
+
""".lstrip()
|
|
34
|
+
|
|
35
|
+
MULTIPLE_PROFILE_MESSAGE = """
|
|
36
|
+
A `dbt_project.yml` file was not found in this directory.
|
|
37
|
+
dbt found the following profiles:
|
|
38
|
+
{}
|
|
39
|
+
|
|
40
|
+
To debug one of these profiles, run:
|
|
41
|
+
dbt debug --profile [profile-name]
|
|
42
|
+
""".lstrip()
|
|
43
|
+
|
|
44
|
+
COULD_NOT_CONNECT_MESSAGE = """
|
|
45
|
+
dbt was unable to connect to the specified database.
|
|
46
|
+
The database returned the following error:
|
|
47
|
+
|
|
48
|
+
>{err}
|
|
49
|
+
|
|
50
|
+
Check your database credentials and try again. For more information, visit:
|
|
51
|
+
{url}
|
|
52
|
+
""".lstrip()
|
|
53
|
+
|
|
54
|
+
MISSING_PROFILE_MESSAGE = """
|
|
55
|
+
dbt looked for a profiles.yml file in {path}, but did
|
|
56
|
+
not find one. For more information on configuring your profile, consult the
|
|
57
|
+
documentation:
|
|
58
|
+
|
|
59
|
+
{url}
|
|
60
|
+
""".lstrip()
|
|
61
|
+
|
|
62
|
+
FILE_NOT_FOUND = "file not found"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
SubtaskStatus = namedtuple(
|
|
66
|
+
"SubtaskStatus", ["log_msg", "run_status", "details", "summary_message"]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DebugRunStatus(Flag):
|
|
71
|
+
SUCCESS = True
|
|
72
|
+
FAIL = False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DebugTask(BaseTask):
|
|
76
|
+
def __init__(self, args: Flags) -> None:
|
|
77
|
+
super().__init__(args)
|
|
78
|
+
self.profiles_dir = args.PROFILES_DIR
|
|
79
|
+
self.profile_path = os.path.join(self.profiles_dir, "profiles.yml")
|
|
80
|
+
try:
|
|
81
|
+
self.project_dir = get_nearest_project_dir(self.args.project_dir)
|
|
82
|
+
except dbt_common.exceptions.DbtBaseException:
|
|
83
|
+
# we probably couldn't find a project directory. Set project dir
|
|
84
|
+
# to whatever was given, or default to the current directory.
|
|
85
|
+
if args.project_dir:
|
|
86
|
+
self.project_dir = args.project_dir
|
|
87
|
+
else:
|
|
88
|
+
self.project_dir = Path.cwd()
|
|
89
|
+
self.project_path = os.path.join(self.project_dir, "dbt_project.yml")
|
|
90
|
+
self.cli_vars: Dict[str, Any] = args.vars
|
|
91
|
+
|
|
92
|
+
# set by _load_*
|
|
93
|
+
self.profile: Optional[Profile] = None
|
|
94
|
+
self.raw_profile_data: Optional[Dict[str, Any]] = None
|
|
95
|
+
self.profile_name: Optional[str] = None
|
|
96
|
+
|
|
97
|
+
def run(self) -> bool:
|
|
98
|
+
# WARN: this is a legacy workflow that is not compatible with other runtime flags
|
|
99
|
+
if self.args.config_dir:
|
|
100
|
+
fire_event(
|
|
101
|
+
OpenCommand(
|
|
102
|
+
open_cmd=dbt_common.clients.system.open_dir_cmd(),
|
|
103
|
+
profiles_dir=str(self.profiles_dir),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
return DebugRunStatus.SUCCESS.value
|
|
107
|
+
|
|
108
|
+
# DVT extension: --all-profiles flag to test all profiles
|
|
109
|
+
if getattr(self.args, "all_profiles", False):
|
|
110
|
+
return self._run_all_profiles_debug()
|
|
111
|
+
|
|
112
|
+
version: str = get_installed_version().to_version_string(skip_matcher=True)
|
|
113
|
+
fire_event(DebugCmdOut(msg="dbt version: {}".format(version)))
|
|
114
|
+
fire_event(DebugCmdOut(msg="python version: {}".format(sys.version.split()[0])))
|
|
115
|
+
fire_event(DebugCmdOut(msg="python path: {}".format(sys.executable)))
|
|
116
|
+
fire_event(DebugCmdOut(msg="os info: {}".format(platform.platform())))
|
|
117
|
+
|
|
118
|
+
# Load profile if possible, then load adapter info (which requires the profile)
|
|
119
|
+
load_profile_status: SubtaskStatus = self._load_profile()
|
|
120
|
+
fire_event(DebugCmdOut(msg="Using profiles dir at {}".format(self.profiles_dir)))
|
|
121
|
+
fire_event(DebugCmdOut(msg="Using profiles.yml file at {}".format(self.profile_path)))
|
|
122
|
+
fire_event(DebugCmdOut(msg="Using dbt_project.yml file at {}".format(self.project_path)))
|
|
123
|
+
if load_profile_status.run_status == RunStatus.Success:
|
|
124
|
+
if self.profile is None:
|
|
125
|
+
raise dbt_common.exceptions.DbtInternalError(
|
|
126
|
+
"Profile should not be None if loading profile completed"
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
adapter_type: str = self.profile.credentials.type
|
|
130
|
+
|
|
131
|
+
adapter_version: str = self._read_adapter_version(
|
|
132
|
+
f"dbt.adapters.{adapter_type}.__version__"
|
|
133
|
+
)
|
|
134
|
+
fire_event(DebugCmdOut(msg="adapter type: {}".format(adapter_type)))
|
|
135
|
+
fire_event(DebugCmdOut(msg="adapter version: {}".format(adapter_version)))
|
|
136
|
+
|
|
137
|
+
# Get project loaded to do additional checks
|
|
138
|
+
load_project_status: SubtaskStatus = self._load_project()
|
|
139
|
+
|
|
140
|
+
dependencies_statuses: List[SubtaskStatus] = []
|
|
141
|
+
if self.args.connection:
|
|
142
|
+
fire_event(DebugCmdOut(msg="Skipping steps before connection verification"))
|
|
143
|
+
else:
|
|
144
|
+
# this job's status not logged since already accounted for in _load_* commands
|
|
145
|
+
self.test_configuration(load_profile_status.log_msg, load_project_status.log_msg)
|
|
146
|
+
dependencies_statuses = self.test_dependencies()
|
|
147
|
+
|
|
148
|
+
# Test connection
|
|
149
|
+
connection_status = self.test_connection()
|
|
150
|
+
|
|
151
|
+
# Log messages from any fails
|
|
152
|
+
all_statuses: List[SubtaskStatus] = [
|
|
153
|
+
load_profile_status,
|
|
154
|
+
load_project_status,
|
|
155
|
+
*dependencies_statuses,
|
|
156
|
+
connection_status,
|
|
157
|
+
]
|
|
158
|
+
all_failing_statuses: List[SubtaskStatus] = list(
|
|
159
|
+
filter(lambda status: status.run_status == RunStatus.Error, all_statuses)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
failure_count: int = len(all_failing_statuses)
|
|
163
|
+
if failure_count > 0:
|
|
164
|
+
fire_event(DebugCmdResult(msg=red(f"{(pluralize(failure_count, 'check'))} failed:")))
|
|
165
|
+
for status in all_failing_statuses:
|
|
166
|
+
fire_event(DebugCmdResult(msg=f"{status.summary_message}\n"))
|
|
167
|
+
return DebugRunStatus.FAIL.value
|
|
168
|
+
else:
|
|
169
|
+
fire_event(DebugCmdResult(msg=green("All checks passed!")))
|
|
170
|
+
return DebugRunStatus.SUCCESS.value
|
|
171
|
+
|
|
172
|
+
# ==============================
|
|
173
|
+
# Override for elsewhere in core
|
|
174
|
+
# ==============================
|
|
175
|
+
|
|
176
|
+
def interpret_results(self, results):
|
|
177
|
+
return results
|
|
178
|
+
|
|
179
|
+
# ===============
|
|
180
|
+
# Loading profile
|
|
181
|
+
# ===============
|
|
182
|
+
|
|
183
|
+
def _load_profile(self) -> SubtaskStatus:
|
|
184
|
+
"""
|
|
185
|
+
Side effects: load self.profile
|
|
186
|
+
load self.target_name
|
|
187
|
+
load self.raw_profile_data
|
|
188
|
+
"""
|
|
189
|
+
if not os.path.exists(self.profile_path):
|
|
190
|
+
return SubtaskStatus(
|
|
191
|
+
log_msg=red("ERROR not found"),
|
|
192
|
+
run_status=RunStatus.Error,
|
|
193
|
+
details=FILE_NOT_FOUND,
|
|
194
|
+
summary_message=MISSING_PROFILE_MESSAGE.format(
|
|
195
|
+
path=self.profile_path, url=ProfileConfigDocs
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
raw_profile_data = load_yaml_text(
|
|
200
|
+
dbt_common.clients.system.load_file_contents(self.profile_path)
|
|
201
|
+
)
|
|
202
|
+
if isinstance(raw_profile_data, dict):
|
|
203
|
+
self.raw_profile_data = raw_profile_data
|
|
204
|
+
|
|
205
|
+
profile_errors = []
|
|
206
|
+
profile_names, summary_message = self._choose_profile_names()
|
|
207
|
+
renderer = ProfileRenderer(self.cli_vars)
|
|
208
|
+
for profile_name in profile_names:
|
|
209
|
+
try:
|
|
210
|
+
profile: Profile = Profile.render(
|
|
211
|
+
renderer,
|
|
212
|
+
profile_name,
|
|
213
|
+
self.args.profile,
|
|
214
|
+
self.args.target,
|
|
215
|
+
# TODO: Generalize safe access to flags.THREADS:
|
|
216
|
+
# https://github.com/dbt-labs/dbt-core/issues/6259
|
|
217
|
+
getattr(self.args, "threads", None),
|
|
218
|
+
)
|
|
219
|
+
except dbt_common.exceptions.DbtConfigError as exc:
|
|
220
|
+
profile_errors.append(str(exc))
|
|
221
|
+
else:
|
|
222
|
+
if len(profile_names) == 1:
|
|
223
|
+
# if a profile was specified, set it on the task
|
|
224
|
+
self.target_name = self._choose_target_name(profile_name)
|
|
225
|
+
self.profile = profile
|
|
226
|
+
|
|
227
|
+
if profile_errors:
|
|
228
|
+
details = "\n\n".join(profile_errors)
|
|
229
|
+
return SubtaskStatus(
|
|
230
|
+
log_msg=red("ERROR invalid"),
|
|
231
|
+
run_status=RunStatus.Error,
|
|
232
|
+
details=details,
|
|
233
|
+
summary_message=(
|
|
234
|
+
summary_message + f"Profile loading failed for the following reason:"
|
|
235
|
+
f"\n{details}"
|
|
236
|
+
f"\n"
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
return SubtaskStatus(
|
|
241
|
+
log_msg=green("OK found and valid"),
|
|
242
|
+
run_status=RunStatus.Success,
|
|
243
|
+
details="",
|
|
244
|
+
summary_message="Profile is valid",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def _choose_profile_names(self) -> Tuple[List[str], str]:
|
|
248
|
+
project_profile: Optional[str] = None
|
|
249
|
+
if os.path.exists(self.project_path):
|
|
250
|
+
try:
|
|
251
|
+
partial = PartialProject.from_project_root(
|
|
252
|
+
os.path.dirname(self.project_path),
|
|
253
|
+
verify_version=bool(self.args.VERSION_CHECK),
|
|
254
|
+
)
|
|
255
|
+
renderer = DbtProjectYamlRenderer(None, self.cli_vars)
|
|
256
|
+
project_profile = partial.render_profile_name(renderer)
|
|
257
|
+
except dbt.exceptions.DbtProjectError:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
args_profile: Optional[str] = getattr(self.args, "profile", None)
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
return [Profile.pick_profile_name(args_profile, project_profile)], ""
|
|
264
|
+
except dbt_common.exceptions.DbtConfigError:
|
|
265
|
+
pass
|
|
266
|
+
# try to guess
|
|
267
|
+
|
|
268
|
+
profiles = []
|
|
269
|
+
if self.raw_profile_data:
|
|
270
|
+
profiles = [k for k in self.raw_profile_data if k != "config"]
|
|
271
|
+
if project_profile is None:
|
|
272
|
+
summary_message = "Could not load dbt_project.yml\n"
|
|
273
|
+
elif len(profiles) == 0:
|
|
274
|
+
summary_message = "The profiles.yml has no profiles\n"
|
|
275
|
+
elif len(profiles) == 1:
|
|
276
|
+
summary_message = ONLY_PROFILE_MESSAGE.format(profiles[0])
|
|
277
|
+
else:
|
|
278
|
+
summary_message = MULTIPLE_PROFILE_MESSAGE.format(
|
|
279
|
+
"\n".join(" - {}".format(o) for o in profiles)
|
|
280
|
+
)
|
|
281
|
+
return profiles, summary_message
|
|
282
|
+
|
|
283
|
+
def _read_adapter_version(self, module) -> str:
|
|
284
|
+
"""read the version out of a standard adapter file"""
|
|
285
|
+
try:
|
|
286
|
+
version = importlib.import_module(module).version
|
|
287
|
+
except ModuleNotFoundError:
|
|
288
|
+
version = red("ERROR not found")
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
version = red("ERROR {}".format(exc))
|
|
291
|
+
raise dbt.exceptions.DbtInternalError(
|
|
292
|
+
f"Error when reading adapter version from {module}: {exc}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return version
|
|
296
|
+
|
|
297
|
+
def _choose_target_name(self, profile_name: str):
|
|
298
|
+
has_raw_profile = (
|
|
299
|
+
self.raw_profile_data is not None and profile_name in self.raw_profile_data
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if not has_raw_profile:
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
# mypy appeasement, we checked just above
|
|
306
|
+
assert self.raw_profile_data is not None
|
|
307
|
+
raw_profile = self.raw_profile_data[profile_name]
|
|
308
|
+
|
|
309
|
+
renderer = ProfileRenderer(self.cli_vars)
|
|
310
|
+
|
|
311
|
+
target_name, _ = Profile.render_profile(
|
|
312
|
+
raw_profile=raw_profile,
|
|
313
|
+
profile_name=profile_name,
|
|
314
|
+
target_override=getattr(self.args, "target", None),
|
|
315
|
+
renderer=renderer,
|
|
316
|
+
)
|
|
317
|
+
return target_name
|
|
318
|
+
|
|
319
|
+
# ===============
|
|
320
|
+
# Loading project
|
|
321
|
+
# ===============
|
|
322
|
+
|
|
323
|
+
def _load_project(self) -> SubtaskStatus:
|
|
324
|
+
"""
|
|
325
|
+
Side effect: load self.project
|
|
326
|
+
"""
|
|
327
|
+
if not os.path.exists(self.project_path):
|
|
328
|
+
return SubtaskStatus(
|
|
329
|
+
log_msg=red("ERROR not found"),
|
|
330
|
+
run_status=RunStatus.Error,
|
|
331
|
+
details=FILE_NOT_FOUND,
|
|
332
|
+
summary_message=(
|
|
333
|
+
f"Project loading failed for the following reason:"
|
|
334
|
+
f"\n project path <{self.project_path}> not found"
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
renderer = DbtProjectYamlRenderer(self.profile, self.cli_vars)
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
self.project = Project.from_project_root(
|
|
342
|
+
str(self.project_dir),
|
|
343
|
+
renderer,
|
|
344
|
+
verify_version=self.args.VERSION_CHECK,
|
|
345
|
+
)
|
|
346
|
+
except dbt_common.exceptions.DbtConfigError as exc:
|
|
347
|
+
return SubtaskStatus(
|
|
348
|
+
log_msg=red("ERROR invalid"),
|
|
349
|
+
run_status=RunStatus.Error,
|
|
350
|
+
details=str(exc),
|
|
351
|
+
summary_message=(
|
|
352
|
+
f"Project loading failed for the following reason:" f"\n{str(exc)}" f"\n"
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
return SubtaskStatus(
|
|
357
|
+
log_msg=green("OK found and valid"),
|
|
358
|
+
run_status=RunStatus.Success,
|
|
359
|
+
details="",
|
|
360
|
+
summary_message="Project is valid",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def _profile_found(self) -> str:
|
|
364
|
+
if not self.raw_profile_data:
|
|
365
|
+
return red("ERROR not found")
|
|
366
|
+
assert self.raw_profile_data is not None
|
|
367
|
+
if self.profile_name in self.raw_profile_data:
|
|
368
|
+
return green("OK found")
|
|
369
|
+
else:
|
|
370
|
+
return red("ERROR not found")
|
|
371
|
+
|
|
372
|
+
def _target_found(self) -> str:
|
|
373
|
+
requirements = self.raw_profile_data and self.profile_name and self.target_name
|
|
374
|
+
if not requirements:
|
|
375
|
+
return red("ERROR not found")
|
|
376
|
+
# mypy appeasement, we checked just above
|
|
377
|
+
assert self.raw_profile_data is not None
|
|
378
|
+
assert self.profile_name is not None
|
|
379
|
+
assert self.target_name is not None
|
|
380
|
+
if self.profile_name not in self.raw_profile_data:
|
|
381
|
+
return red("ERROR not found")
|
|
382
|
+
profiles = self.raw_profile_data[self.profile_name]["outputs"]
|
|
383
|
+
if self.target_name not in profiles:
|
|
384
|
+
return red("ERROR not found")
|
|
385
|
+
else:
|
|
386
|
+
return green("OK found")
|
|
387
|
+
|
|
388
|
+
# ============
|
|
389
|
+
# Config tests
|
|
390
|
+
# ============
|
|
391
|
+
|
|
392
|
+
def test_git(self) -> SubtaskStatus:
|
|
393
|
+
try:
|
|
394
|
+
dbt_common.clients.system.run_cmd(os.getcwd(), ["git", "--help"])
|
|
395
|
+
except dbt_common.exceptions.ExecutableError as exc:
|
|
396
|
+
return SubtaskStatus(
|
|
397
|
+
log_msg=red("ERROR"),
|
|
398
|
+
run_status=RunStatus.Error,
|
|
399
|
+
details="git error",
|
|
400
|
+
summary_message="Error from git --help: {!s}".format(exc),
|
|
401
|
+
)
|
|
402
|
+
else:
|
|
403
|
+
return SubtaskStatus(
|
|
404
|
+
log_msg=green("OK found"),
|
|
405
|
+
run_status=RunStatus.Success,
|
|
406
|
+
details="",
|
|
407
|
+
summary_message="git is installed and on the path",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def test_dependencies(self) -> List[SubtaskStatus]:
|
|
411
|
+
fire_event(DebugCmdOut(msg="Required dependencies:"))
|
|
412
|
+
|
|
413
|
+
git_test_status = self.test_git()
|
|
414
|
+
fire_event(DebugCmdResult(msg=f" - git [{git_test_status.log_msg}]\n"))
|
|
415
|
+
|
|
416
|
+
return [git_test_status]
|
|
417
|
+
|
|
418
|
+
def test_configuration(self, profile_status_msg, project_status_msg):
|
|
419
|
+
fire_event(DebugCmdOut(msg="Configuration:"))
|
|
420
|
+
fire_event(DebugCmdOut(msg=f" profiles.yml file [{profile_status_msg}]"))
|
|
421
|
+
fire_event(DebugCmdOut(msg=f" dbt_project.yml file [{project_status_msg}]"))
|
|
422
|
+
|
|
423
|
+
# skip profile stuff if we can't find a profile name
|
|
424
|
+
if self.profile_name is not None:
|
|
425
|
+
fire_event(
|
|
426
|
+
DebugCmdOut(
|
|
427
|
+
msg=" profile: {} [{}]\n".format(self.profile_name, self._profile_found())
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
fire_event(
|
|
431
|
+
DebugCmdOut(
|
|
432
|
+
msg=" target: {} [{}]\n".format(self.target_name, self._target_found())
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# ===============
|
|
437
|
+
# Connection test
|
|
438
|
+
# ===============
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def attempt_connection(profile) -> Optional[str]:
|
|
442
|
+
"""Return a string containing the error message, or None if there was no error."""
|
|
443
|
+
register_adapter(profile, get_mp_context())
|
|
444
|
+
adapter = get_adapter(profile)
|
|
445
|
+
try:
|
|
446
|
+
with adapter.connection_named("debug"):
|
|
447
|
+
# is defined in adapter class
|
|
448
|
+
adapter.debug_query()
|
|
449
|
+
except Exception as exc:
|
|
450
|
+
return COULD_NOT_CONNECT_MESSAGE.format(
|
|
451
|
+
err=str(exc),
|
|
452
|
+
url=ProfileConfigDocs,
|
|
453
|
+
)
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
def test_connection(self) -> SubtaskStatus:
|
|
457
|
+
if self.profile is None:
|
|
458
|
+
fire_event(DebugCmdOut(msg="Connection test skipped since no profile was found"))
|
|
459
|
+
return SubtaskStatus(
|
|
460
|
+
log_msg=red("SKIPPED"),
|
|
461
|
+
run_status=RunStatus.Skipped,
|
|
462
|
+
details="No profile found",
|
|
463
|
+
summary_message="Connection test skipped since no profile was found",
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
fire_event(DebugCmdOut(msg="Connection:"))
|
|
467
|
+
for k, v in self.profile.credentials.connection_info():
|
|
468
|
+
fire_event(DebugCmdOut(msg=f" {k}: {v}"))
|
|
469
|
+
|
|
470
|
+
connection_result = self.attempt_connection(self.profile)
|
|
471
|
+
if connection_result is None:
|
|
472
|
+
status = SubtaskStatus(
|
|
473
|
+
log_msg=green("OK connection ok"),
|
|
474
|
+
run_status=RunStatus.Success,
|
|
475
|
+
details="",
|
|
476
|
+
summary_message="Connection test passed",
|
|
477
|
+
)
|
|
478
|
+
else:
|
|
479
|
+
status = SubtaskStatus(
|
|
480
|
+
log_msg=red("ERROR"),
|
|
481
|
+
run_status=RunStatus.Error,
|
|
482
|
+
details="Failure in connecting to db",
|
|
483
|
+
summary_message=connection_result,
|
|
484
|
+
)
|
|
485
|
+
fire_event(DebugCmdOut(msg=f" Connection test: [{status.log_msg}]\n"))
|
|
486
|
+
return status
|
|
487
|
+
|
|
488
|
+
@classmethod
|
|
489
|
+
def validate_connection(cls, target_dict) -> None:
|
|
490
|
+
"""Validate a connection dictionary. On error, raises a DbtConfigError."""
|
|
491
|
+
target_name = "test"
|
|
492
|
+
# make a fake profile that we can parse
|
|
493
|
+
profile_data = {
|
|
494
|
+
"outputs": {
|
|
495
|
+
target_name: target_dict,
|
|
496
|
+
},
|
|
497
|
+
}
|
|
498
|
+
# this will raise a DbtConfigError on failure
|
|
499
|
+
profile = Profile.from_raw_profile_info(
|
|
500
|
+
raw_profile=profile_data,
|
|
501
|
+
profile_name="",
|
|
502
|
+
target_override=target_name,
|
|
503
|
+
renderer=ProfileRenderer({}),
|
|
504
|
+
)
|
|
505
|
+
result = cls.attempt_connection(profile)
|
|
506
|
+
if result is not None:
|
|
507
|
+
raise dbt.exceptions.DbtProfileError(result, result_type="connection_failure")
|
|
508
|
+
|
|
509
|
+
# ===========================
|
|
510
|
+
# DVT extension: --all-profiles
|
|
511
|
+
# ===========================
|
|
512
|
+
|
|
513
|
+
def _run_all_profiles_debug(self) -> bool:
|
|
514
|
+
"""
|
|
515
|
+
Run debug tests for all configured profiles.
|
|
516
|
+
|
|
517
|
+
This is a DVT extension that tests all profiles in profiles.yml,
|
|
518
|
+
not just the project's default profile.
|
|
519
|
+
"""
|
|
520
|
+
from pathlib import Path
|
|
521
|
+
|
|
522
|
+
from dvt.config.profiles_v2 import ProfileRegistry, load_unified_profiles
|
|
523
|
+
|
|
524
|
+
version: str = get_installed_version().to_version_string(skip_matcher=True)
|
|
525
|
+
fire_event(DebugCmdOut(msg="=" * 60))
|
|
526
|
+
fire_event(DebugCmdOut(msg="DVT Multi-Profile Debug"))
|
|
527
|
+
fire_event(DebugCmdOut(msg="=" * 60))
|
|
528
|
+
fire_event(DebugCmdOut(msg="dbt version: {}".format(version)))
|
|
529
|
+
fire_event(DebugCmdOut(msg="python version: {}".format(sys.version.split()[0])))
|
|
530
|
+
fire_event(DebugCmdOut(msg="python path: {}".format(sys.executable)))
|
|
531
|
+
fire_event(DebugCmdOut(msg="os info: {}".format(platform.platform())))
|
|
532
|
+
fire_event(DebugCmdOut(msg=""))
|
|
533
|
+
fire_event(DebugCmdOut(msg="Using profiles dir at {}".format(self.profiles_dir)))
|
|
534
|
+
fire_event(DebugCmdOut(msg="Using profiles.yml file at {}".format(self.profile_path)))
|
|
535
|
+
fire_event(DebugCmdOut(msg=""))
|
|
536
|
+
|
|
537
|
+
# Load unified profiles
|
|
538
|
+
project_dir = Path(self.project_dir) if self.project_dir else None
|
|
539
|
+
unified_profiles = load_unified_profiles(project_dir)
|
|
540
|
+
|
|
541
|
+
# Create registry
|
|
542
|
+
registry = ProfileRegistry(unified_profiles)
|
|
543
|
+
|
|
544
|
+
# Get all profiles
|
|
545
|
+
all_profiles = registry.list_all_profiles()
|
|
546
|
+
|
|
547
|
+
if not all_profiles:
|
|
548
|
+
fire_event(DebugCmdOut(msg=red("No profiles found in profiles.yml")))
|
|
549
|
+
return DebugRunStatus.FAIL.value
|
|
550
|
+
|
|
551
|
+
fire_event(DebugCmdOut(msg=f"Found {len(all_profiles)} profile(s):"))
|
|
552
|
+
for profile_name in all_profiles:
|
|
553
|
+
fire_event(DebugCmdOut(msg=f" - {profile_name}"))
|
|
554
|
+
fire_event(DebugCmdOut(msg=""))
|
|
555
|
+
|
|
556
|
+
# Test each profile
|
|
557
|
+
results = {}
|
|
558
|
+
for profile_name in all_profiles:
|
|
559
|
+
fire_event(DebugCmdOut(msg="=" * 60))
|
|
560
|
+
fire_event(DebugCmdOut(msg=f"Testing profile: {profile_name}"))
|
|
561
|
+
fire_event(DebugCmdOut(msg="=" * 60))
|
|
562
|
+
|
|
563
|
+
try:
|
|
564
|
+
# Get profile config
|
|
565
|
+
profile_config = registry.get_or_create_profile(profile_name)
|
|
566
|
+
if not profile_config:
|
|
567
|
+
fire_event(DebugCmdOut(msg=red(f" Profile '{profile_name}' not found")))
|
|
568
|
+
results[profile_name] = False
|
|
569
|
+
continue
|
|
570
|
+
|
|
571
|
+
# Show profile info
|
|
572
|
+
adapter_type = profile_config.get("type", "unknown")
|
|
573
|
+
fire_event(DebugCmdOut(msg=f" Adapter type: {adapter_type}"))
|
|
574
|
+
|
|
575
|
+
# Show connection info (without sensitive data)
|
|
576
|
+
fire_event(DebugCmdOut(msg=" Connection info:"))
|
|
577
|
+
for key, value in profile_config.items():
|
|
578
|
+
if key not in ("type", "threads", "password", "token", "private_key"):
|
|
579
|
+
fire_event(DebugCmdOut(msg=f" {key}: {value}"))
|
|
580
|
+
|
|
581
|
+
# Test connection
|
|
582
|
+
fire_event(DebugCmdOut(msg=" Testing connection..."))
|
|
583
|
+
|
|
584
|
+
# Create a minimal Profile object for testing
|
|
585
|
+
try:
|
|
586
|
+
# Create profile data structure
|
|
587
|
+
profile_data = {
|
|
588
|
+
"outputs": {
|
|
589
|
+
"test": profile_config,
|
|
590
|
+
},
|
|
591
|
+
"target": "test",
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
# Render profile
|
|
595
|
+
renderer = ProfileRenderer(self.cli_vars)
|
|
596
|
+
test_profile = Profile.from_raw_profile_info(
|
|
597
|
+
raw_profile=profile_data,
|
|
598
|
+
profile_name=profile_name,
|
|
599
|
+
target_override="test",
|
|
600
|
+
renderer=renderer,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# Attempt connection
|
|
604
|
+
connection_result = self.attempt_connection(test_profile)
|
|
605
|
+
|
|
606
|
+
if connection_result is None:
|
|
607
|
+
fire_event(DebugCmdOut(msg=green(" ✓ Connection successful")))
|
|
608
|
+
results[profile_name] = True
|
|
609
|
+
else:
|
|
610
|
+
fire_event(DebugCmdOut(msg=red(" ✗ Connection failed")))
|
|
611
|
+
fire_event(DebugCmdOut(msg=f" Error: {connection_result}"))
|
|
612
|
+
results[profile_name] = False
|
|
613
|
+
|
|
614
|
+
except Exception as e:
|
|
615
|
+
fire_event(DebugCmdOut(msg=red(" ✗ Connection test failed")))
|
|
616
|
+
fire_event(DebugCmdOut(msg=f" Error: {str(e)}"))
|
|
617
|
+
results[profile_name] = False
|
|
618
|
+
|
|
619
|
+
except Exception as e:
|
|
620
|
+
fire_event(DebugCmdOut(msg=red(f" Error testing profile: {str(e)}")))
|
|
621
|
+
results[profile_name] = False
|
|
622
|
+
|
|
623
|
+
fire_event(DebugCmdOut(msg=""))
|
|
624
|
+
|
|
625
|
+
# Summary
|
|
626
|
+
fire_event(DebugCmdOut(msg="=" * 60))
|
|
627
|
+
fire_event(DebugCmdOut(msg="Summary"))
|
|
628
|
+
fire_event(DebugCmdOut(msg="=" * 60))
|
|
629
|
+
|
|
630
|
+
success_count = sum(1 for success in results.values() if success)
|
|
631
|
+
fail_count = len(results) - success_count
|
|
632
|
+
|
|
633
|
+
fire_event(DebugCmdOut(msg=f"Total profiles tested: {len(results)}"))
|
|
634
|
+
fire_event(DebugCmdOut(msg=green(f"Successful: {success_count}")))
|
|
635
|
+
if fail_count > 0:
|
|
636
|
+
fire_event(DebugCmdOut(msg=red(f"Failed: {fail_count}")))
|
|
637
|
+
|
|
638
|
+
fire_event(DebugCmdOut(msg=""))
|
|
639
|
+
fire_event(DebugCmdOut(msg="Profile results:"))
|
|
640
|
+
for profile_name, success in results.items():
|
|
641
|
+
status = green("✓") if success else red("✗")
|
|
642
|
+
fire_event(DebugCmdOut(msg=f" {status} {profile_name}"))
|
|
643
|
+
|
|
644
|
+
# Return overall success
|
|
645
|
+
if fail_count == 0:
|
|
646
|
+
fire_event(DebugCmdResult(msg=green("\nAll profiles passed!")))
|
|
647
|
+
return DebugRunStatus.SUCCESS.value
|
|
648
|
+
else:
|
|
649
|
+
fire_event(DebugCmdResult(msg=red(f"\n{pluralize(fail_count, 'profile')} failed")))
|
|
650
|
+
return DebugRunStatus.FAIL.value
|