validio-cli 0.14.0__tar.gz → 0.16.0__tar.gz
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.
- {validio_cli-0.14.0 → validio_cli-0.16.0}/PKG-INFO +2 -2
- {validio_cli-0.14.0 → validio_cli-0.16.0}/pyproject.toml +2 -2
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/code.py +13 -4
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/config.py +4 -1
- validio_cli-0.16.0/validio_cli/bin/entities/dbt.py +120 -0
- validio_cli-0.16.0/validio_cli/bin/entities/incidents.py +155 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/notification_rules.py +0 -9
- validio_cli-0.14.0/validio_cli/bin/entities/dbt.py +0 -55
- validio_cli-0.14.0/validio_cli/bin/entities/incidents.py +0 -256
- {validio_cli-0.14.0 → validio_cli-0.16.0}/LICENSE +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/README_PUBLIC.md +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/__init__.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/channels.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/credentials.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/metrics.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/recommendations.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/resources.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/segmentations.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/segments.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/sources.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/users.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/validators.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/entities/windows.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/bin/main.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/components.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/metadata.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/namespace.py +0 -0
- {validio_cli-0.14.0 → validio_cli-0.16.0}/validio_cli/schema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: validio-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: CLI tool to interact with the Validio platform
|
|
5
5
|
Home-page: https://validio.io/
|
|
6
6
|
License: Apache-2.0
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Requires-Dist: camel-converter (>=3.0.0,<4.0.0)
|
|
15
|
-
Requires-Dist: classdiff (
|
|
15
|
+
Requires-Dist: classdiff (==0.4.0)
|
|
16
16
|
Requires-Dist: prompt-toolkit (>=3.0.38,<4.0.0)
|
|
17
17
|
Requires-Dist: pydantic (==2.4.2)
|
|
18
18
|
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
@@ -3,7 +3,7 @@ name = "validio-cli"
|
|
|
3
3
|
# This version does not represent the released version or any tag. For each
|
|
4
4
|
# release we automatically bump this before building and publishing so this
|
|
5
5
|
# should be kept at 0.0.1dev1
|
|
6
|
-
version = "0.
|
|
6
|
+
version = "0.16.0"
|
|
7
7
|
description = "CLI tool to interact with the Validio platform"
|
|
8
8
|
authors = ["Validio <support@validio.io>"]
|
|
9
9
|
license = "Apache-2.0"
|
|
@@ -15,7 +15,7 @@ readme = "README_PUBLIC.md"
|
|
|
15
15
|
[tool.poetry.dependencies]
|
|
16
16
|
python = "^3.10"
|
|
17
17
|
camel-converter = "^3.0.0"
|
|
18
|
-
classdiff = "
|
|
18
|
+
classdiff = "0.4.0"
|
|
19
19
|
validio-sdk = "*"
|
|
20
20
|
typer = { extras = ["all"], version = "^0.7.0" }
|
|
21
21
|
prompt-toolkit = "^3.0.38"
|
|
@@ -426,16 +426,25 @@ def _show_update_resource_diff(
|
|
|
426
426
|
diff_output: DiffOutput,
|
|
427
427
|
) -> None:
|
|
428
428
|
for r in resources.values():
|
|
429
|
-
class_key = r.manifest.
|
|
429
|
+
class_key = r.manifest.__class__.__name__
|
|
430
430
|
|
|
431
|
-
print(f"{class_key} '{r.manifest.
|
|
431
|
+
print(f"{class_key} '{r.manifest.name}' will be updated")
|
|
432
432
|
|
|
433
433
|
rewrites = _diff_field_rewrites(show_schema)
|
|
434
|
+
|
|
434
435
|
a_value = code_plan._create_resource_diff_object(
|
|
435
|
-
r.manifest
|
|
436
|
+
r.manifest,
|
|
437
|
+
show_secrets=show_secrets,
|
|
438
|
+
rewrites=rewrites,
|
|
439
|
+
secrets_changed=r.secrets_changed,
|
|
440
|
+
is_manifest=True,
|
|
436
441
|
)
|
|
437
442
|
b_value = code_plan._create_resource_diff_object(
|
|
438
|
-
r.server
|
|
443
|
+
r.server,
|
|
444
|
+
show_secrets=show_secrets,
|
|
445
|
+
rewrites=rewrites,
|
|
446
|
+
secrets_changed=r.secrets_changed,
|
|
447
|
+
is_manifest=False,
|
|
439
448
|
)
|
|
440
449
|
|
|
441
450
|
_show_diff(b_value, a_value, color, diff_output, class_key)
|
|
@@ -77,6 +77,7 @@ def ns(
|
|
|
77
77
|
def get(
|
|
78
78
|
config_dir: str = ConfigDir,
|
|
79
79
|
output_format: OutputFormat = OutputFormatOption,
|
|
80
|
+
show_secrets: bool = False,
|
|
80
81
|
) -> None:
|
|
81
82
|
config_path = Path(config_dir) if config_dir is not None else None
|
|
82
83
|
|
|
@@ -109,7 +110,9 @@ def get(
|
|
|
109
110
|
"default_namespace": validio_config.default_namespace,
|
|
110
111
|
"endpoint": validio_config.endpoint,
|
|
111
112
|
"access_key": validio_config.access_key,
|
|
112
|
-
"access_secret": validio_config.
|
|
113
|
+
"access_secret": validio_config._access_secret
|
|
114
|
+
if show_secrets
|
|
115
|
+
else validio_config.access_secret,
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
if output_format == OutputFormat.JSON:
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from validio_sdk import ValidioError, dbt, util
|
|
7
|
+
|
|
8
|
+
from validio_cli import AsyncTyper, ConfigDir, Namespace, get_client_and_config
|
|
9
|
+
from validio_cli.bin.entities import credentials
|
|
10
|
+
|
|
11
|
+
app = AsyncTyper(help="dbt related commands")
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
Well-known file name for the manifest artifact.
|
|
15
|
+
|
|
16
|
+
https://docs.getdbt.com/reference/artifacts/dbt-artifacts
|
|
17
|
+
"""
|
|
18
|
+
MANIFEST_FILE_NAME = "manifest.json"
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
Well-known file name for the run-results artifact.
|
|
22
|
+
|
|
23
|
+
https://docs.getdbt.com/reference/artifacts/dbt-artifacts
|
|
24
|
+
"""
|
|
25
|
+
RUN_RESULTS_FILE_NAME = "run_results.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _resolve_file_paths(
|
|
29
|
+
explicit_manifest_path: Optional[Path],
|
|
30
|
+
explicit_run_result_path: Optional[Path],
|
|
31
|
+
target_path: Optional[Path],
|
|
32
|
+
) -> Tuple[Path, Optional[Path]]:
|
|
33
|
+
if explicit_manifest_path or explicit_run_result_path:
|
|
34
|
+
print(
|
|
35
|
+
"Deprecation notice ⚠️: The --manifest and --run-results flags "
|
|
36
|
+
"have been deprecated and will be removed in a future version. "
|
|
37
|
+
"Please use the --target-path argument instead."
|
|
38
|
+
)
|
|
39
|
+
if not explicit_manifest_path:
|
|
40
|
+
raise ValidioError(
|
|
41
|
+
"--manifest is required when combined with the --run-results argument"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return explicit_manifest_path, explicit_run_result_path
|
|
45
|
+
|
|
46
|
+
if not target_path:
|
|
47
|
+
raise ValidioError("Missing --target-path argument")
|
|
48
|
+
|
|
49
|
+
manifest_path = target_path / MANIFEST_FILE_NAME
|
|
50
|
+
run_results_path = target_path / RUN_RESULTS_FILE_NAME
|
|
51
|
+
if not manifest_path.is_file():
|
|
52
|
+
raise ValidioError(f"manifest file not found at {manifest_path}")
|
|
53
|
+
|
|
54
|
+
if not run_results_path.is_file():
|
|
55
|
+
print(
|
|
56
|
+
"Warning: run_results file not found at {run_results_path}. "
|
|
57
|
+
"Uploading only manifest file"
|
|
58
|
+
)
|
|
59
|
+
return manifest_path, None
|
|
60
|
+
|
|
61
|
+
return manifest_path, run_results_path
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.async_command(help="Upload dbt artifact")
|
|
65
|
+
async def upload(
|
|
66
|
+
config_dir: str = ConfigDir,
|
|
67
|
+
namespace: str = Namespace(),
|
|
68
|
+
credential_id: str = typer.Option(..., help="Credential name or ID"),
|
|
69
|
+
manifest: Optional[Path] = typer.Option(
|
|
70
|
+
help="Path to the manifest file. "
|
|
71
|
+
"(deprecated in favor of the --target-path flag)",
|
|
72
|
+
default=None,
|
|
73
|
+
),
|
|
74
|
+
job_name: str = typer.Option(
|
|
75
|
+
..., help="The job that the dbt execution belongs to, e.g. `staging-pipeline`"
|
|
76
|
+
),
|
|
77
|
+
run_results: Optional[Path] = typer.Option(
|
|
78
|
+
help="Path to the run results file. "
|
|
79
|
+
"(deprecated in favor of the --target-path flag)",
|
|
80
|
+
default=None,
|
|
81
|
+
),
|
|
82
|
+
target_path: Optional[Path] = typer.Option(
|
|
83
|
+
help="Path to the dbt artifacts target directory. "
|
|
84
|
+
"(supersedes the --manifest flag)",
|
|
85
|
+
default=None,
|
|
86
|
+
),
|
|
87
|
+
) -> None:
|
|
88
|
+
vc, cfg = await get_client_and_config(config_dir)
|
|
89
|
+
|
|
90
|
+
resolved_credential_id = await credentials.get_credential_id(
|
|
91
|
+
vc, cfg, credential_id, namespace
|
|
92
|
+
)
|
|
93
|
+
if resolved_credential_id is None:
|
|
94
|
+
raise ValidioError(f"Credential '{credential_id}' not found")
|
|
95
|
+
|
|
96
|
+
manifest, run_results = _resolve_file_paths(manifest, run_results, target_path)
|
|
97
|
+
|
|
98
|
+
run_results_content = None
|
|
99
|
+
try:
|
|
100
|
+
manifest_content = typing.cast(dict[str, Any], util.read_json_file(manifest))
|
|
101
|
+
if run_results is not None:
|
|
102
|
+
run_results_content = typing.cast(
|
|
103
|
+
dict[str, Any],
|
|
104
|
+
util.read_json_file(run_results),
|
|
105
|
+
)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise ValidioError(f"Failed to process artifacts file: {e}")
|
|
108
|
+
|
|
109
|
+
await dbt.upload_artifacts(
|
|
110
|
+
client=vc,
|
|
111
|
+
credential_id=resolved_credential_id,
|
|
112
|
+
job_name=job_name,
|
|
113
|
+
manifest=manifest_content,
|
|
114
|
+
run_results=run_results_content,
|
|
115
|
+
)
|
|
116
|
+
return print("dbt artifact uploaded successfully")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
typer.run(app())
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from validio_sdk import ValidioError
|
|
6
|
+
from validio_sdk.graphql_client import (
|
|
7
|
+
GetSourceIncidentsSource,
|
|
8
|
+
GetValidatorIncidentsValidator,
|
|
9
|
+
)
|
|
10
|
+
from validio_sdk.graphql_client.input_types import TimeRangeInput
|
|
11
|
+
|
|
12
|
+
from validio_cli import (
|
|
13
|
+
AsyncTyper,
|
|
14
|
+
ConfigDir,
|
|
15
|
+
Namespace,
|
|
16
|
+
OutputFormat,
|
|
17
|
+
OutputFormatOption,
|
|
18
|
+
OutputSettings,
|
|
19
|
+
_format_relative_time,
|
|
20
|
+
get_client_and_config,
|
|
21
|
+
output_json,
|
|
22
|
+
output_text,
|
|
23
|
+
)
|
|
24
|
+
from validio_cli.bin.entities import sources, validators
|
|
25
|
+
|
|
26
|
+
app = AsyncTyper(help="Incidents from validators")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.async_command(
|
|
30
|
+
help="""List all incidents.
|
|
31
|
+
|
|
32
|
+
By default you will get incidents from the last hour. You can specify a time
|
|
33
|
+
range for when the incident occurred by specifying when the incident ended.
|
|
34
|
+
|
|
35
|
+
You can list incidents in different ways:
|
|
36
|
+
|
|
37
|
+
* Listing all incidents
|
|
38
|
+
|
|
39
|
+
* Listing all source incidents for a specific source with --source
|
|
40
|
+
|
|
41
|
+
* Listing all incidents for a specific validator with --validator
|
|
42
|
+
|
|
43
|
+
* Listing all incidents for a specific segment with --segment
|
|
44
|
+
|
|
45
|
+
* Listing all incidents for a specific validator and segment with --validator
|
|
46
|
+
and --segment together
|
|
47
|
+
"""
|
|
48
|
+
)
|
|
49
|
+
async def get(
|
|
50
|
+
config_dir: str = ConfigDir,
|
|
51
|
+
output_format: OutputFormat = OutputFormatOption,
|
|
52
|
+
namespace: str = Namespace(),
|
|
53
|
+
ended_before: datetime = typer.Option(
|
|
54
|
+
datetime.utcnow(),
|
|
55
|
+
help="The incident ended before this timestamp",
|
|
56
|
+
),
|
|
57
|
+
ended_after: datetime = typer.Option(
|
|
58
|
+
(datetime.utcnow() - timedelta(hours=1)),
|
|
59
|
+
help="The incident ended after this timestamp",
|
|
60
|
+
),
|
|
61
|
+
validator: str = typer.Option(None, help="Validator to fetch incidents for"),
|
|
62
|
+
segment: str = typer.Option(None, help="Segment to fetch incidents for"),
|
|
63
|
+
source: str = typer.Option(None, help="Source to fetch incidents for"),
|
|
64
|
+
) -> None:
|
|
65
|
+
vc, cfg = await get_client_and_config(config_dir)
|
|
66
|
+
|
|
67
|
+
if validator is not None:
|
|
68
|
+
validator_id = await validators.get_validator_id(vc, cfg, validator, namespace)
|
|
69
|
+
if validator_id is None:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
if source is not None:
|
|
73
|
+
source_id = await sources.get_source_id(vc, cfg, source, namespace)
|
|
74
|
+
if source_id is None:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# TODO(UI-2006): These should all support namespace
|
|
78
|
+
|
|
79
|
+
incidents: (None | GetValidatorIncidentsValidator | GetSourceIncidentsSource) = None
|
|
80
|
+
|
|
81
|
+
if validator:
|
|
82
|
+
incidents = await vc.get_validator_incidents(
|
|
83
|
+
id=validator_id,
|
|
84
|
+
range=TimeRangeInput(
|
|
85
|
+
start=ended_after,
|
|
86
|
+
end=ended_before,
|
|
87
|
+
),
|
|
88
|
+
segment_id=segment,
|
|
89
|
+
)
|
|
90
|
+
elif source:
|
|
91
|
+
incidents = await vc.get_source_incidents(
|
|
92
|
+
id=source_id,
|
|
93
|
+
range=TimeRangeInput(
|
|
94
|
+
start=ended_after,
|
|
95
|
+
end=ended_before,
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
raise ValidioError(
|
|
100
|
+
"You need to specify one of --validator and/or --segment or --source"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if not incidents:
|
|
104
|
+
return output_text(None, {})
|
|
105
|
+
|
|
106
|
+
if output_format == OutputFormat.JSON:
|
|
107
|
+
return output_json(incidents)
|
|
108
|
+
|
|
109
|
+
return output_text(
|
|
110
|
+
incidents.incidents,
|
|
111
|
+
fields={
|
|
112
|
+
"operator": OutputSettings(
|
|
113
|
+
attribute_name="metric",
|
|
114
|
+
reformat=calculate_operator,
|
|
115
|
+
),
|
|
116
|
+
"bound": OutputSettings(
|
|
117
|
+
attribute_name="metric",
|
|
118
|
+
reformat=calculate_bound,
|
|
119
|
+
),
|
|
120
|
+
"value": OutputSettings(
|
|
121
|
+
attribute_name="metric",
|
|
122
|
+
reformat=lambda x: x.value,
|
|
123
|
+
),
|
|
124
|
+
"age": OutputSettings(
|
|
125
|
+
attribute_name="metric",
|
|
126
|
+
reformat=lambda x: _format_relative_time(x.end_time),
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def calculate_operator(item: Any) -> str:
|
|
133
|
+
type_ = item.typename__[len("ValidatorMetricWith") :]
|
|
134
|
+
if type_ == "DynamicThreshold":
|
|
135
|
+
operator = item.decision_bounds_type
|
|
136
|
+
else:
|
|
137
|
+
operator = item.operator
|
|
138
|
+
|
|
139
|
+
return f"{type_}/{operator}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def calculate_bound(item: Any) -> str:
|
|
143
|
+
type_ = item.typename__[len("ValidatorMetricWith") :]
|
|
144
|
+
if type_ == "DynamicThreshold":
|
|
145
|
+
bound = f"{item.lower_bound:.2f} - {item.upper_bound:.2f}"
|
|
146
|
+
elif type_ == "FixedThreshold":
|
|
147
|
+
bound = item.bound
|
|
148
|
+
else:
|
|
149
|
+
bound = "-"
|
|
150
|
+
|
|
151
|
+
return bound
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
typer.run(app())
|
|
@@ -28,9 +28,6 @@ async def get(
|
|
|
28
28
|
output_format: OutputFormat = OutputFormatOption,
|
|
29
29
|
namespace: str = Namespace(),
|
|
30
30
|
identifier: str = Identifier,
|
|
31
|
-
source: str = typer.Option(
|
|
32
|
-
None, help="List notification rules for this source (ID)"
|
|
33
|
-
),
|
|
34
31
|
) -> None:
|
|
35
32
|
vc, cfg = await get_client_and_config(config_dir)
|
|
36
33
|
|
|
@@ -46,12 +43,6 @@ async def get(
|
|
|
46
43
|
filter=ResourceFilter(resource_namespace=get_namespace(namespace, cfg))
|
|
47
44
|
)
|
|
48
45
|
|
|
49
|
-
notification_rules = [
|
|
50
|
-
nr
|
|
51
|
-
for nr in notification_rules
|
|
52
|
-
if source is None or (nr is not None and source in nr.sources)
|
|
53
|
-
]
|
|
54
|
-
|
|
55
46
|
# TODO(UI-2311): Fully support list/get/get_by_resource_name
|
|
56
47
|
notification_rules = _single_resource_if_specified(notification_rules, identifier)
|
|
57
48
|
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import typing
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
import typer
|
|
7
|
-
from validio_sdk import ValidioError, dbt, util
|
|
8
|
-
from validio_sdk.graphql_client.input_types import DbtArtifactUploadInput
|
|
9
|
-
|
|
10
|
-
from validio_cli import AsyncTyper, ConfigDir, Namespace, get_client_and_config
|
|
11
|
-
from validio_cli.bin.entities import credentials
|
|
12
|
-
|
|
13
|
-
app = AsyncTyper(help="dbt related commands")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@app.async_command(help="Upload dbt artifact")
|
|
17
|
-
async def upload(
|
|
18
|
-
config_dir: str = ConfigDir,
|
|
19
|
-
namespace: str = Namespace(),
|
|
20
|
-
credential_id: str = typer.Option(..., help="Credential name or ID"),
|
|
21
|
-
manifest: Path = typer.Option(..., help="Path to the manifest file"),
|
|
22
|
-
run_results: Path = typer.Option(help="Path to the run results file", default=None),
|
|
23
|
-
) -> None:
|
|
24
|
-
vc, cfg = await get_client_and_config(config_dir)
|
|
25
|
-
|
|
26
|
-
resolved_credential_id = await credentials.get_credential_id(
|
|
27
|
-
vc, cfg, credential_id, namespace
|
|
28
|
-
)
|
|
29
|
-
if resolved_credential_id is None:
|
|
30
|
-
raise ValidioError(f"Credential '{credential_id}' not found")
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
manifest_content = util.read_json_file(manifest)
|
|
34
|
-
trimmed_manifest = dbt.trim_manifest_json(
|
|
35
|
-
typing.cast(dict[str, Any], manifest_content)
|
|
36
|
-
)
|
|
37
|
-
except Exception as e:
|
|
38
|
-
raise ValidioError(f"Failed to process manifest file: {e}")
|
|
39
|
-
|
|
40
|
-
run_results_content = None
|
|
41
|
-
if run_results is not None:
|
|
42
|
-
run_results_content = json.dumps(util.read_json_file(run_results))
|
|
43
|
-
|
|
44
|
-
await vc.dbt_artifact_upload(
|
|
45
|
-
input=DbtArtifactUploadInput(
|
|
46
|
-
credential_id=resolved_credential_id,
|
|
47
|
-
manifest=json.dumps(trimmed_manifest),
|
|
48
|
-
run_results=run_results_content,
|
|
49
|
-
)
|
|
50
|
-
)
|
|
51
|
-
return print("dbt artifact uploaded successfully")
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if __name__ == "__main__":
|
|
55
|
-
typer.run(app())
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from datetime import datetime, timedelta
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
import typer
|
|
6
|
-
from validio_sdk import ValidioError
|
|
7
|
-
from validio_sdk.graphql_client import (
|
|
8
|
-
GetSegmentIncidentsSegmentIncidents,
|
|
9
|
-
GetSourceIncidentsSourceIncidentsSegmentLimitExceededNotification,
|
|
10
|
-
GetValidatorIncidentsValidatorIncidents,
|
|
11
|
-
GetValidatorSegmentIncidentsValidatorSegmentIncidents,
|
|
12
|
-
)
|
|
13
|
-
from validio_sdk.graphql_client.input_types import (
|
|
14
|
-
SegmentIncidentsInput,
|
|
15
|
-
SourceIncidentsInput,
|
|
16
|
-
TimeRangeInput,
|
|
17
|
-
ValidatorIncidentsInput,
|
|
18
|
-
ValidatorSegmentIncidentsInput,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
from validio_cli import (
|
|
22
|
-
AsyncTyper,
|
|
23
|
-
ConfigDir,
|
|
24
|
-
Namespace,
|
|
25
|
-
OutputFormat,
|
|
26
|
-
OutputFormatOption,
|
|
27
|
-
OutputSettings,
|
|
28
|
-
_format_relative_time,
|
|
29
|
-
get_client_and_config,
|
|
30
|
-
output_json,
|
|
31
|
-
output_text,
|
|
32
|
-
)
|
|
33
|
-
from validio_cli.bin.entities import sources, validators
|
|
34
|
-
|
|
35
|
-
app = AsyncTyper(help="Incidents from validators")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@app.async_command(
|
|
39
|
-
help="""List all incidents.
|
|
40
|
-
|
|
41
|
-
By default you will get incidents from the last hour. You can specify a time
|
|
42
|
-
range for when the incident occurred by specifying when the incident ended.
|
|
43
|
-
|
|
44
|
-
You can list incidents in different ways:
|
|
45
|
-
|
|
46
|
-
* Listing all incidents
|
|
47
|
-
|
|
48
|
-
* Listing all source incidents for a specific source with --source
|
|
49
|
-
|
|
50
|
-
* Listing all incidents for a specific validator with --validator
|
|
51
|
-
|
|
52
|
-
* Listing all incidents for a specific segment with --segment
|
|
53
|
-
|
|
54
|
-
* Listing all incidents for a specific validator and segment with --validator
|
|
55
|
-
and --segment together
|
|
56
|
-
"""
|
|
57
|
-
)
|
|
58
|
-
async def get(
|
|
59
|
-
config_dir: str = ConfigDir,
|
|
60
|
-
output_format: OutputFormat = OutputFormatOption,
|
|
61
|
-
namespace: str = Namespace(),
|
|
62
|
-
ended_before: datetime = typer.Option(
|
|
63
|
-
datetime.utcnow(),
|
|
64
|
-
help="The incident ended before this timestamp",
|
|
65
|
-
),
|
|
66
|
-
ended_after: datetime = typer.Option(
|
|
67
|
-
(datetime.utcnow() - timedelta(hours=1)),
|
|
68
|
-
help="The incident ended after this timestamp",
|
|
69
|
-
),
|
|
70
|
-
validator: str = typer.Option(None, help="Validator to fetch incidents for"),
|
|
71
|
-
segment: str = typer.Option(None, help="Segment to fetch incidents for"),
|
|
72
|
-
source: str = typer.Option(None, help="Source to fetch incidents for"),
|
|
73
|
-
) -> None:
|
|
74
|
-
vc, cfg = await get_client_and_config(config_dir)
|
|
75
|
-
|
|
76
|
-
if validator is not None:
|
|
77
|
-
validator_id = await validators.get_validator_id(vc, cfg, validator, namespace)
|
|
78
|
-
if validator_id is None:
|
|
79
|
-
return None
|
|
80
|
-
|
|
81
|
-
if source is not None:
|
|
82
|
-
source_id = await sources.get_source_id(vc, cfg, source, namespace)
|
|
83
|
-
if source_id is None:
|
|
84
|
-
return None
|
|
85
|
-
|
|
86
|
-
# TODO(UI-2006): These should all support namespace
|
|
87
|
-
|
|
88
|
-
incidents: (
|
|
89
|
-
None
|
|
90
|
-
| list[GetValidatorIncidentsValidatorIncidents]
|
|
91
|
-
| list[GetValidatorSegmentIncidentsValidatorSegmentIncidents]
|
|
92
|
-
| list[GetSegmentIncidentsSegmentIncidents]
|
|
93
|
-
) = None
|
|
94
|
-
|
|
95
|
-
if validator and segment:
|
|
96
|
-
incidents = await vc.get_validator_segment_incidents(
|
|
97
|
-
ValidatorSegmentIncidentsInput(
|
|
98
|
-
time_range=TimeRangeInput(
|
|
99
|
-
start=ended_after,
|
|
100
|
-
end=ended_before,
|
|
101
|
-
),
|
|
102
|
-
validator_id=validator_id,
|
|
103
|
-
segment_id=segment,
|
|
104
|
-
)
|
|
105
|
-
)
|
|
106
|
-
elif validator:
|
|
107
|
-
incidents = await vc.get_validator_incidents(
|
|
108
|
-
ValidatorIncidentsInput(
|
|
109
|
-
time_range=TimeRangeInput(
|
|
110
|
-
start=ended_after,
|
|
111
|
-
end=ended_before,
|
|
112
|
-
),
|
|
113
|
-
validator_id=validator_id,
|
|
114
|
-
)
|
|
115
|
-
)
|
|
116
|
-
elif segment:
|
|
117
|
-
incidents = await vc.get_segment_incidents(
|
|
118
|
-
SegmentIncidentsInput(
|
|
119
|
-
time_range=TimeRangeInput(
|
|
120
|
-
start=ended_after,
|
|
121
|
-
end=ended_before,
|
|
122
|
-
),
|
|
123
|
-
segment_id=segment,
|
|
124
|
-
)
|
|
125
|
-
)
|
|
126
|
-
elif source:
|
|
127
|
-
# Since source incidents looks completely different we hand them off to
|
|
128
|
-
# a separate function, we can't re-use any of the rendering.
|
|
129
|
-
return _output_text_source_incidents(
|
|
130
|
-
output_format,
|
|
131
|
-
await vc.get_source_incidents(
|
|
132
|
-
SourceIncidentsInput(
|
|
133
|
-
time_range=TimeRangeInput(
|
|
134
|
-
start=ended_after,
|
|
135
|
-
end=ended_before,
|
|
136
|
-
),
|
|
137
|
-
source_id=source_id,
|
|
138
|
-
)
|
|
139
|
-
),
|
|
140
|
-
)
|
|
141
|
-
else:
|
|
142
|
-
raise ValidioError(
|
|
143
|
-
"You need to specify one of --validator and/or --segment or --source"
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
if output_format == OutputFormat.JSON:
|
|
147
|
-
return output_json(incidents)
|
|
148
|
-
|
|
149
|
-
return output_text(
|
|
150
|
-
incidents,
|
|
151
|
-
fields={
|
|
152
|
-
"operator": OutputSettings(
|
|
153
|
-
attribute_name="metric",
|
|
154
|
-
reformat=calculate_operator,
|
|
155
|
-
),
|
|
156
|
-
"bound": OutputSettings(
|
|
157
|
-
attribute_name="metric",
|
|
158
|
-
reformat=calculate_bound,
|
|
159
|
-
),
|
|
160
|
-
"value": OutputSettings(
|
|
161
|
-
attribute_name="metric",
|
|
162
|
-
reformat=lambda x: x.value,
|
|
163
|
-
),
|
|
164
|
-
"age": OutputSettings(
|
|
165
|
-
attribute_name="metric",
|
|
166
|
-
reformat=lambda x: _format_relative_time(x.end_time),
|
|
167
|
-
),
|
|
168
|
-
},
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _output_text_source_incidents(
|
|
173
|
-
output_format: OutputFormat, incidents: list[Any]
|
|
174
|
-
) -> None:
|
|
175
|
-
if output_format == OutputFormat.JSON:
|
|
176
|
-
return output_json(incidents)
|
|
177
|
-
|
|
178
|
-
@dataclass
|
|
179
|
-
class SchemaChange:
|
|
180
|
-
field: str
|
|
181
|
-
change_type: str
|
|
182
|
-
old_value: str
|
|
183
|
-
new_value: str
|
|
184
|
-
|
|
185
|
-
change_type_to_key = {
|
|
186
|
-
"Nullability": "nullable",
|
|
187
|
-
"JtdType": "type",
|
|
188
|
-
"UnderlyingType": "underlying_type",
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
changes = []
|
|
192
|
-
for item in incidents:
|
|
193
|
-
# TODO: We only show schema changes for now
|
|
194
|
-
if item.typename__ != "SchemaChangeNotification":
|
|
195
|
-
continue
|
|
196
|
-
|
|
197
|
-
for field, field_changes in item.payload.items():
|
|
198
|
-
for ct in field_changes["change_types"]:
|
|
199
|
-
k = change_type_to_key.get(ct)
|
|
200
|
-
|
|
201
|
-
changes.append(
|
|
202
|
-
SchemaChange(
|
|
203
|
-
field=field,
|
|
204
|
-
change_type=ct,
|
|
205
|
-
old_value=field_changes["old"][k] if k else "-",
|
|
206
|
-
new_value=field_changes["new"][k] if k else "-",
|
|
207
|
-
)
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
return output_text(
|
|
211
|
-
changes,
|
|
212
|
-
fields={
|
|
213
|
-
"field": None,
|
|
214
|
-
"change_type": None,
|
|
215
|
-
"old_value": None,
|
|
216
|
-
"new_value": None,
|
|
217
|
-
},
|
|
218
|
-
)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def calculate_operator(item: Any) -> str:
|
|
222
|
-
type_ = item.typename__[len("ValidatorMetricWith") :]
|
|
223
|
-
if type_ == "DynamicThreshold":
|
|
224
|
-
operator = item.decision_bounds_type
|
|
225
|
-
else:
|
|
226
|
-
operator = item.operator
|
|
227
|
-
|
|
228
|
-
return f"{type_}/{operator}"
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def calculate_bound(item: Any) -> str:
|
|
232
|
-
type_ = item.typename__[len("ValidatorMetricWith") :]
|
|
233
|
-
if type_ == "DynamicThreshold":
|
|
234
|
-
bound = f"{item.lower_bound:.2f} - {item.upper_bound:.2f}"
|
|
235
|
-
elif type_ == "FixedThreshold":
|
|
236
|
-
bound = item.bound
|
|
237
|
-
else:
|
|
238
|
-
bound = "-"
|
|
239
|
-
|
|
240
|
-
return bound
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def _output_text_source_incidents_segment_limit_exceeded(
|
|
244
|
-
incidents: list[GetSourceIncidentsSourceIncidentsSegmentLimitExceededNotification],
|
|
245
|
-
) -> None:
|
|
246
|
-
return output_text(
|
|
247
|
-
incidents,
|
|
248
|
-
fields={
|
|
249
|
-
"limit": None,
|
|
250
|
-
"age": OutputSettings(attribute_name="created_at"),
|
|
251
|
-
},
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if __name__ == "__main__":
|
|
256
|
-
typer.run(app())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|