recce-nightly 1.2.0.20250506__py3-none-any.whl → 1.26.0.20251124__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 recce-nightly might be problematic. Click here for more details.
- recce/VERSION +1 -1
- recce/__init__.py +27 -22
- recce/adapter/base.py +11 -14
- recce/adapter/dbt_adapter/__init__.py +810 -480
- recce/adapter/dbt_adapter/dbt_version.py +3 -0
- recce/adapter/sqlmesh_adapter.py +24 -35
- recce/apis/check_api.py +39 -28
- recce/apis/check_func.py +33 -27
- recce/apis/run_api.py +25 -19
- recce/apis/run_func.py +29 -23
- recce/artifact.py +119 -51
- recce/cli.py +1299 -323
- recce/config.py +42 -33
- recce/connect_to_cloud.py +138 -0
- recce/core.py +55 -47
- recce/data/404.html +1 -1
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +5 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
- recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
- recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
- recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
- recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
- recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
- recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
- recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
- recce/data/_next/static/chunks/99d638224186c118.js +1 -0
- recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
- recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
- recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
- recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
- recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_next/static/media/reload-image.7aa931c7.svg +4 -0
- recce/data/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._head.txt +8 -0
- recce/data/_not-found/__next._index.txt +8 -0
- recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
- recce/data/_not-found/__next._not-found.txt +4 -0
- recce/data/_not-found/__next._tree.txt +3 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +68 -0
- recce/data/imgs/reload-image.svg +4 -0
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -7
- recce/diff.py +6 -12
- recce/event/__init__.py +86 -74
- recce/event/collector.py +33 -22
- recce/event/track.py +49 -27
- recce/exceptions.py +1 -1
- recce/git.py +7 -7
- recce/github.py +57 -53
- recce/mcp_server.py +716 -0
- recce/models/__init__.py +4 -1
- recce/models/check.py +6 -7
- recce/models/run.py +1 -0
- recce/models/types.py +131 -28
- recce/pull_request.py +27 -25
- recce/run.py +165 -121
- recce/server.py +303 -111
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +632 -0
- recce/state/const.py +26 -0
- recce/state/local.py +56 -0
- recce/state/state.py +119 -0
- recce/state/state_loader.py +174 -0
- recce/summary.py +188 -143
- recce/tasks/__init__.py +19 -3
- recce/tasks/core.py +11 -13
- recce/tasks/dataframe.py +82 -18
- recce/tasks/histogram.py +69 -34
- recce/tasks/lineage.py +2 -2
- recce/tasks/profile.py +152 -86
- recce/tasks/query.py +139 -87
- recce/tasks/rowcount.py +37 -31
- recce/tasks/schema.py +18 -15
- recce/tasks/top_k.py +35 -35
- recce/tasks/valuediff.py +216 -152
- recce/util/__init__.py +3 -0
- recce/util/api_token.py +80 -0
- recce/util/breaking.py +87 -85
- recce/util/cll.py +274 -219
- recce/util/io.py +22 -17
- recce/util/lineage.py +65 -16
- recce/util/logger.py +1 -1
- recce/util/onboarding_state.py +45 -0
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +322 -72
- recce/util/singleton.py +4 -4
- recce/yaml/__init__.py +7 -10
- recce_cloud/__init__.py +24 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +111 -0
- recce_cloud/api/client.py +150 -0
- recce_cloud/api/exceptions.py +26 -0
- recce_cloud/api/factory.py +63 -0
- recce_cloud/api/github.py +76 -0
- recce_cloud/api/gitlab.py +82 -0
- recce_cloud/artifact.py +57 -0
- recce_cloud/ci_providers/__init__.py +9 -0
- recce_cloud/ci_providers/base.py +82 -0
- recce_cloud/ci_providers/detector.py +147 -0
- recce_cloud/ci_providers/github_actions.py +136 -0
- recce_cloud/ci_providers/gitlab_ci.py +130 -0
- recce_cloud/cli.py +245 -0
- recce_cloud/upload.py +214 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
- recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/conftest.py +9 -5
- tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
- tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
- tests/adapter/dbt_adapter/test_selector.py +22 -21
- tests/recce_cloud/__init__.py +0 -0
- tests/recce_cloud/test_ci_providers.py +351 -0
- tests/recce_cloud/test_cli.py +372 -0
- tests/recce_cloud/test_client.py +273 -0
- tests/recce_cloud/test_platform_clients.py +333 -0
- tests/tasks/conftest.py +1 -1
- tests/tasks/test_histogram.py +58 -66
- tests/tasks/test_lineage.py +36 -23
- tests/tasks/test_preset_checks.py +45 -31
- tests/tasks/test_profile.py +339 -15
- tests/tasks/test_query.py +46 -46
- tests/tasks/test_row_count.py +65 -46
- tests/tasks/test_schema.py +65 -42
- tests/tasks/test_top_k.py +22 -18
- tests/tasks/test_valuediff.py +43 -32
- tests/test_cli.py +174 -60
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_config.py +7 -9
- tests/test_connect_to_cloud.py +82 -0
- tests/test_core.py +151 -4
- tests/test_dbt.py +7 -7
- tests/test_mcp_server.py +332 -0
- tests/test_pull_request.py +1 -1
- tests/test_server.py +25 -19
- tests/test_summary.py +29 -17
- recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
- recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
- recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
- recce/data/_next/static/chunks/368-7587b306577df275.js +0 -65
- recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
- recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
- recce/data/_next/static/chunks/3a92ee20-3b5d922d4157af5e.js +0 -1
- recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
- recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
- recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
- recce/data/_next/static/chunks/6ef81909-694dc38134099299.js +0 -1
- recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
- recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
- recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
- recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
- recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
- recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
- recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
- recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
- recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
- recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
- recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
- recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
- recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
- recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
- recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
- recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
- recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/state.py +0 -753
- recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
- tests/test_state.py +0 -123
- /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@ class DbtVersion:
|
|
|
2
2
|
|
|
3
3
|
def __init__(self):
|
|
4
4
|
from dbt import version as dbt_version
|
|
5
|
+
|
|
5
6
|
dbt_version = self.parse(dbt_version.__version__)
|
|
6
7
|
if dbt_version.is_prerelease:
|
|
7
8
|
dbt_version = self.parse(dbt_version.base_version)
|
|
@@ -10,10 +11,12 @@ class DbtVersion:
|
|
|
10
11
|
@staticmethod
|
|
11
12
|
def parse(version: str):
|
|
12
13
|
from packaging import version as v
|
|
14
|
+
|
|
13
15
|
return v.parse(version)
|
|
14
16
|
|
|
15
17
|
def as_version(self, other):
|
|
16
18
|
from packaging.version import Version
|
|
19
|
+
|
|
17
20
|
if isinstance(other, Version):
|
|
18
21
|
return other
|
|
19
22
|
if isinstance(other, str):
|
recce/adapter/sqlmesh_adapter.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import typing as t
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Dict, Optional, Type
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
6
|
-
from sqlglot import
|
|
6
|
+
from sqlglot import Expression, parse_one, select
|
|
7
7
|
from sqlglot.optimizer import traverse_scope
|
|
8
8
|
from sqlmesh.core.context import Context as SqlmeshContext
|
|
9
9
|
from sqlmesh.core.environment import Environment
|
|
@@ -11,7 +11,7 @@ from sqlmesh.core.state_sync import StateReader
|
|
|
11
11
|
|
|
12
12
|
from recce.adapter.base import BaseAdapter
|
|
13
13
|
from recce.models import RunType
|
|
14
|
-
from recce.tasks import
|
|
14
|
+
from recce.tasks import QueryDiffTask, QueryTask, RowCountDiffTask, Task
|
|
15
15
|
|
|
16
16
|
sqlmesh_supported_registry: Dict[RunType, Type[Task]] = {
|
|
17
17
|
RunType.QUERY: QueryTask,
|
|
@@ -37,7 +37,7 @@ class SqlmeshAdapter(BaseAdapter):
|
|
|
37
37
|
|
|
38
38
|
for snapshot in state_reader.get_snapshots(env.snapshots, hydrate_seeds=True).values():
|
|
39
39
|
|
|
40
|
-
if snapshot.node_type.lower() !=
|
|
40
|
+
if snapshot.node_type.lower() != "model":
|
|
41
41
|
continue
|
|
42
42
|
|
|
43
43
|
model = snapshot.model
|
|
@@ -45,16 +45,16 @@ class SqlmeshAdapter(BaseAdapter):
|
|
|
45
45
|
continue
|
|
46
46
|
|
|
47
47
|
node = {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
"unique_id": model.name,
|
|
49
|
+
"name": model.name,
|
|
50
|
+
"resource_type": snapshot.node_type.lower(),
|
|
51
|
+
"checksum": {"checksum": snapshot.fingerprint.data_hash},
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
columns = {}
|
|
55
55
|
for column, type in model.columns_to_types.items():
|
|
56
|
-
columns[column] = {
|
|
57
|
-
node[
|
|
56
|
+
columns[column] = {"name": column, "type": str(type)}
|
|
57
|
+
node["columns"] = columns
|
|
58
58
|
|
|
59
59
|
nodes[snapshot.name] = node
|
|
60
60
|
parents = [snapshotId.name for snapshotId in snapshot.parents]
|
|
@@ -81,15 +81,15 @@ class SqlmeshAdapter(BaseAdapter):
|
|
|
81
81
|
|
|
82
82
|
@classmethod
|
|
83
83
|
def load(cls, **kwargs):
|
|
84
|
-
sqlmesh_envs = kwargs.get(
|
|
84
|
+
sqlmesh_envs = kwargs.get("sqlmesh_envs")
|
|
85
85
|
if sqlmesh_envs is None:
|
|
86
|
-
raise Exception('
|
|
86
|
+
raise Exception("'--sqlmesh-envs SOURCE:TARGET' is required")
|
|
87
87
|
|
|
88
|
-
envs = sqlmesh_envs.split(
|
|
88
|
+
envs = sqlmesh_envs.split(":")
|
|
89
89
|
if len(envs) != 2:
|
|
90
90
|
raise Exception('sqlmesh_envs must be in the format of "SOURCE:TARGET"')
|
|
91
91
|
|
|
92
|
-
sqlmesh_config = kwargs.get(
|
|
92
|
+
sqlmesh_config = kwargs.get("sqlmesh_config", None)
|
|
93
93
|
context = SqlmeshContext(config=sqlmesh_config)
|
|
94
94
|
base_env = context.state_reader.get_environment(envs[0])
|
|
95
95
|
curr_env = context.state_reader.get_environment(envs[1])
|
|
@@ -100,18 +100,14 @@ class SqlmeshAdapter(BaseAdapter):
|
|
|
100
100
|
|
|
101
101
|
return cls(context=context, base_env=base_env, curr_env=curr_env)
|
|
102
102
|
|
|
103
|
-
def replace_virtual_tables(
|
|
104
|
-
|
|
105
|
-
sql: t.Union[Expression, str],
|
|
106
|
-
base: bool = None
|
|
107
|
-
) -> Expression:
|
|
108
|
-
'''
|
|
103
|
+
def replace_virtual_tables(self, sql: t.Union[Expression, str], base: bool = None) -> Expression:
|
|
104
|
+
"""
|
|
109
105
|
Replace virtual tables based on the env name.
|
|
110
106
|
|
|
111
107
|
Args:
|
|
112
108
|
sql: SQL expression to replace virtual tables
|
|
113
109
|
base: True: replace virtual tables with base env, False: replace virtual tables with current env, None: no replacement
|
|
114
|
-
|
|
110
|
+
"""
|
|
115
111
|
if isinstance(sql, str):
|
|
116
112
|
expression = parse_one(sql, dialect=self.context.default_dialect)
|
|
117
113
|
else:
|
|
@@ -119,30 +115,23 @@ class SqlmeshAdapter(BaseAdapter):
|
|
|
119
115
|
|
|
120
116
|
if base is not None:
|
|
121
117
|
env = self.base_env if base else self.curr_env
|
|
122
|
-
if env.name !=
|
|
118
|
+
if env.name != "prod":
|
|
123
119
|
model_names = [model.name for model in self.context.models.values()]
|
|
124
120
|
for scope in traverse_scope(expression):
|
|
125
121
|
for table in scope.tables:
|
|
126
|
-
if f
|
|
127
|
-
table.args[
|
|
122
|
+
if f"{table.db}.{table.name}" in model_names:
|
|
123
|
+
table.args["db"] = f"{table.args['db']}__{env.name}"
|
|
128
124
|
|
|
129
125
|
return expression
|
|
130
126
|
|
|
131
127
|
def fetchdf_with_limit(
|
|
132
|
-
self,
|
|
133
|
-
sql: t.Union[Expression, str],
|
|
134
|
-
base: Optional[bool] = None,
|
|
135
|
-
limit: Optional[int] = None
|
|
128
|
+
self, sql: t.Union[Expression, str], base: Optional[bool] = None, limit: Optional[int] = None
|
|
136
129
|
) -> (pd.DataFrame, bool):
|
|
137
130
|
expression = self.replace_virtual_tables(sql, base=base)
|
|
138
131
|
if limit:
|
|
139
|
-
expression =
|
|
140
|
-
|
|
141
|
-
)
|
|
142
|
-
'__QUERY'
|
|
143
|
-
).with_(
|
|
144
|
-
'__QUERY', as_=expression
|
|
145
|
-
).limit(limit + 1 if limit else None)
|
|
132
|
+
expression = (
|
|
133
|
+
select("*").from_("__QUERY").with_("__QUERY", as_=expression).limit(limit + 1 if limit else None)
|
|
134
|
+
)
|
|
146
135
|
df = self.context.fetchdf(expression)
|
|
147
136
|
if limit and len(df) > limit:
|
|
148
137
|
df = df.head(limit)
|
recce/apis/check_api.py
CHANGED
|
@@ -2,23 +2,27 @@ from datetime import datetime, timezone
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
from uuid import UUID
|
|
4
4
|
|
|
5
|
-
from fastapi import APIRouter,
|
|
5
|
+
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
8
|
-
from recce.apis.check_func import
|
|
8
|
+
from recce.apis.check_func import (
|
|
9
|
+
create_check_from_run,
|
|
10
|
+
create_check_without_run,
|
|
11
|
+
export_persistent_state,
|
|
12
|
+
)
|
|
9
13
|
from recce.apis.run_func import submit_run
|
|
10
14
|
from recce.event import log_api_event
|
|
11
15
|
from recce.exceptions import RecceException
|
|
12
|
-
from recce.models import
|
|
16
|
+
from recce.models import Check, CheckDAO, Run, RunDAO, RunType
|
|
13
17
|
|
|
14
|
-
check_router = APIRouter(tags=[
|
|
18
|
+
check_router = APIRouter(tags=["check"])
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
class CreateCheckIn(BaseModel):
|
|
18
22
|
name: Optional[str] = None
|
|
19
|
-
description: str =
|
|
23
|
+
description: str = ""
|
|
20
24
|
run_id: Optional[str] = None
|
|
21
|
-
type: Optional[RunType] = None,
|
|
25
|
+
type: Optional[RunType] = (None,)
|
|
22
26
|
params: Optional[dict] = None
|
|
23
27
|
view_options: Optional[dict] = None
|
|
24
28
|
track_props: Optional[dict] = None
|
|
@@ -39,16 +43,17 @@ class CheckOut(BaseModel):
|
|
|
39
43
|
def from_check(cls, check: Check):
|
|
40
44
|
check_related_runs = RunDAO().list_by_check_id(check.check_id)
|
|
41
45
|
last_run = check_related_runs[-1] if len(check_related_runs) > 0 else None
|
|
42
|
-
return CheckOut(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
return CheckOut(
|
|
47
|
+
check_id=check.check_id,
|
|
48
|
+
name=check.name,
|
|
49
|
+
description=check.description,
|
|
50
|
+
type=check.type.value,
|
|
51
|
+
params=check.params,
|
|
52
|
+
view_options=check.view_options,
|
|
53
|
+
is_checked=check.is_checked,
|
|
54
|
+
is_preset=check.is_preset,
|
|
55
|
+
last_run=last_run,
|
|
56
|
+
)
|
|
52
57
|
|
|
53
58
|
|
|
54
59
|
@check_router.post("/checks", status_code=201, response_model=CheckOut)
|
|
@@ -59,7 +64,7 @@ async def create_check(check_in: CreateCheckIn, background_tasks: BackgroundTask
|
|
|
59
64
|
check_in.run_id,
|
|
60
65
|
check_name=check_in.name,
|
|
61
66
|
check_description=check_in.description,
|
|
62
|
-
check_view_options=check_in.view_options
|
|
67
|
+
check_view_options=check_in.view_options,
|
|
63
68
|
)
|
|
64
69
|
else:
|
|
65
70
|
check = create_check_without_run(
|
|
@@ -67,12 +72,15 @@ async def create_check(check_in: CreateCheckIn, background_tasks: BackgroundTask
|
|
|
67
72
|
check_description=check_in.description,
|
|
68
73
|
check_type=check_in.type,
|
|
69
74
|
params=check_in.params,
|
|
70
|
-
check_view_options=check_in.view_options
|
|
75
|
+
check_view_options=check_in.view_options,
|
|
71
76
|
)
|
|
72
|
-
log_api_event(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
log_api_event(
|
|
78
|
+
"create_check",
|
|
79
|
+
dict(
|
|
80
|
+
type=str(check.type),
|
|
81
|
+
track_props=check_in.track_props,
|
|
82
|
+
),
|
|
83
|
+
)
|
|
76
84
|
except NameError as e:
|
|
77
85
|
raise HTTPException(status_code=404, detail=str(e))
|
|
78
86
|
except ValueError as e:
|
|
@@ -93,10 +101,13 @@ async def run_check_handler(check_id: UUID, input: RunCheckIn):
|
|
|
93
101
|
raise HTTPException(status_code=404, detail=f"Check ID '{check_id}' not found")
|
|
94
102
|
|
|
95
103
|
try:
|
|
96
|
-
log_api_event(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
log_api_event(
|
|
105
|
+
"rerun_check",
|
|
106
|
+
dict(
|
|
107
|
+
type=str(check.type),
|
|
108
|
+
rerun=True,
|
|
109
|
+
),
|
|
110
|
+
)
|
|
100
111
|
run, future = submit_run(check.type, check.params, check_id=check_id)
|
|
101
112
|
except RecceException as e:
|
|
102
113
|
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -122,7 +133,7 @@ async def list_checks_handler():
|
|
|
122
133
|
async def get_check_handler(check_id: UUID):
|
|
123
134
|
check = CheckDAO().find_check_by_id(check_id)
|
|
124
135
|
if check is None:
|
|
125
|
-
raise HTTPException(status_code=404, detail=
|
|
136
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
126
137
|
|
|
127
138
|
runs = RunDAO().list_by_check_id(check_id)
|
|
128
139
|
last_run = runs[-1] if len(runs) > 0 else None
|
|
@@ -144,7 +155,7 @@ class PatchCheckIn(BaseModel):
|
|
|
144
155
|
async def update_check_handler(check_id: UUID, patch: PatchCheckIn, background_tasks: BackgroundTasks):
|
|
145
156
|
check = CheckDAO().find_check_by_id(check_id)
|
|
146
157
|
if check is None:
|
|
147
|
-
raise HTTPException(status_code=404, detail=
|
|
158
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
148
159
|
|
|
149
160
|
if patch.name is not None:
|
|
150
161
|
check.name = patch.name
|
recce/apis/check_func.py
CHANGED
|
@@ -5,13 +5,13 @@ from fastapi import HTTPException
|
|
|
5
5
|
|
|
6
6
|
from recce.apis.run_func import generate_run_name
|
|
7
7
|
from recce.core import default_context
|
|
8
|
-
from recce.models import
|
|
8
|
+
from recce.models import Check, CheckDAO, RunDAO, RunType
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def validate_schema_diff_check(params):
|
|
12
|
-
node_id = params.get(
|
|
12
|
+
node_id = params.get("node_id")
|
|
13
13
|
if node_id is None:
|
|
14
|
-
raise HTTPException(status_code=400, detail=
|
|
14
|
+
raise HTTPException(status_code=400, detail="node_id is required for schema diff")
|
|
15
15
|
node_name = default_context().get_node_name_by_id(node_id)
|
|
16
16
|
if node_name is None:
|
|
17
17
|
raise HTTPException(status_code=400, detail=f"node_id '{node_id}' not found in dbt manifest")
|
|
@@ -45,8 +45,8 @@ def _get_ref_model(sql_template: str) -> Optional[str]:
|
|
|
45
45
|
def _generate_check_name(check_type, params, view_options):
|
|
46
46
|
now = datetime.utcnow().strftime("%d %b %Y")
|
|
47
47
|
if check_type == RunType.SCHEMA_DIFF:
|
|
48
|
-
if params.get(
|
|
49
|
-
nodeIds = params.get(
|
|
48
|
+
if params.get("node_id"):
|
|
49
|
+
nodeIds = params.get("node_id") if isinstance(params.get("node_id"), list) else [params.get("node_id")]
|
|
50
50
|
if len(nodeIds) == 1:
|
|
51
51
|
node_name = get_node_name_by_id(nodeIds[0])
|
|
52
52
|
return f"schema diff of {node_name}".capitalize()
|
|
@@ -54,7 +54,7 @@ def _generate_check_name(check_type, params, view_options):
|
|
|
54
54
|
return f"schema diff of {len(nodeIds)} nodes".capitalize()
|
|
55
55
|
return f"{'schema diff'.capitalize()} - {now}"
|
|
56
56
|
elif check_type == RunType.LINEAGE_DIFF:
|
|
57
|
-
nodes = view_options.get(
|
|
57
|
+
nodes = view_options.get("node_ids") if view_options else params.get("node_ids")
|
|
58
58
|
if nodes is not None:
|
|
59
59
|
return f"lineage diff of {len(nodes)} nodes".capitalize()
|
|
60
60
|
return f"{'lineage diff'.capitalize()} - {now}"
|
|
@@ -62,10 +62,11 @@ def _generate_check_name(check_type, params, view_options):
|
|
|
62
62
|
return f"{'check'.capitalize()} - {now}"
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
def create_check_from_run(
|
|
66
|
-
|
|
65
|
+
def create_check_from_run(
|
|
66
|
+
run_id, check_name=None, check_description="", check_view_options=None, is_preset=False, is_checked=False
|
|
67
|
+
):
|
|
67
68
|
if run_id is None:
|
|
68
|
-
raise ValueError(
|
|
69
|
+
raise ValueError("run_id is required")
|
|
69
70
|
|
|
70
71
|
run = RunDAO().find_run_by_id(run_id)
|
|
71
72
|
if run is None:
|
|
@@ -76,29 +77,34 @@ def create_check_from_run(run_id, check_name=None, check_description='', check_v
|
|
|
76
77
|
|
|
77
78
|
_validate_check(run_type, run_params)
|
|
78
79
|
name = check_name if check_name is not None else generate_run_name(run)
|
|
79
|
-
check = Check(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
check = Check(
|
|
81
|
+
name=name,
|
|
82
|
+
description=check_description,
|
|
83
|
+
type=run_type,
|
|
84
|
+
params=run_params,
|
|
85
|
+
view_options=check_view_options,
|
|
86
|
+
is_preset=is_preset,
|
|
87
|
+
is_checked=is_checked,
|
|
88
|
+
)
|
|
86
89
|
CheckDAO().create(check)
|
|
87
90
|
run.check_id = check.check_id
|
|
88
91
|
|
|
89
92
|
return check
|
|
90
93
|
|
|
91
94
|
|
|
92
|
-
def create_check_without_run(
|
|
93
|
-
|
|
95
|
+
def create_check_without_run(
|
|
96
|
+
check_name, check_description, check_type, params, check_view_options, is_preset=False, is_checked=False
|
|
97
|
+
):
|
|
94
98
|
name = check_name if check_name is not None else _generate_check_name(check_type, params, check_view_options)
|
|
95
|
-
check = Check(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
check = Check(
|
|
100
|
+
name=name,
|
|
101
|
+
description=check_description,
|
|
102
|
+
type=check_type,
|
|
103
|
+
params=params,
|
|
104
|
+
view_options=check_view_options,
|
|
105
|
+
is_preset=is_preset,
|
|
106
|
+
is_checked=is_checked,
|
|
107
|
+
)
|
|
102
108
|
CheckDAO().create(check)
|
|
103
109
|
return check
|
|
104
110
|
|
|
@@ -119,6 +125,6 @@ def export_persistent_state():
|
|
|
119
125
|
if state_loader is not None:
|
|
120
126
|
is_conflict = state_loader.check_conflict()
|
|
121
127
|
if is_conflict:
|
|
122
|
-
ctx.sync_state(
|
|
128
|
+
ctx.sync_state("merge")
|
|
123
129
|
else:
|
|
124
|
-
ctx.sync_state(
|
|
130
|
+
ctx.sync_state("overwrite")
|
recce/apis/run_api.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import List, Optional
|
|
3
3
|
from uuid import UUID
|
|
4
4
|
|
|
5
5
|
from fastapi import APIRouter, HTTPException, Query
|
|
6
6
|
from pydantic import BaseModel
|
|
7
7
|
|
|
8
|
-
from recce.apis.run_func import
|
|
8
|
+
from recce.apis.run_func import cancel_run, materialize_run_results, submit_run
|
|
9
9
|
from recce.event import log_api_event
|
|
10
10
|
from recce.exceptions import RecceException
|
|
11
11
|
from recce.models import RunDAO
|
|
12
12
|
|
|
13
|
-
run_router = APIRouter(tags=[
|
|
13
|
+
run_router = APIRouter(tags=["run"])
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class CreateRunIn(BaseModel):
|
|
@@ -23,10 +23,13 @@ class CreateRunIn(BaseModel):
|
|
|
23
23
|
|
|
24
24
|
@run_router.post("/runs", status_code=201)
|
|
25
25
|
async def create_run_handler(input: CreateRunIn):
|
|
26
|
-
log_api_event(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
log_api_event(
|
|
27
|
+
"create_run",
|
|
28
|
+
dict(
|
|
29
|
+
type=input.type,
|
|
30
|
+
track_props=input.track_props,
|
|
31
|
+
),
|
|
32
|
+
)
|
|
30
33
|
try:
|
|
31
34
|
run, future = submit_run(input.type, input.params)
|
|
32
35
|
except RecceException as e:
|
|
@@ -51,7 +54,7 @@ async def cancel_run_handler(run_id: UUID):
|
|
|
51
54
|
async def wait_run_handler(run_id: UUID, timeout: int = Query(None, description="Maximum number of seconds to wait")):
|
|
52
55
|
run = RunDAO().find_run_by_id(run_id)
|
|
53
56
|
if run is None:
|
|
54
|
-
raise HTTPException(status_code=404, detail=
|
|
57
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
55
58
|
|
|
56
59
|
start_time = asyncio.get_event_loop().time()
|
|
57
60
|
while run.result is None and run.error is None:
|
|
@@ -65,18 +68,21 @@ async def wait_run_handler(run_id: UUID, timeout: int = Query(None, description=
|
|
|
65
68
|
async def list_run_handler():
|
|
66
69
|
runs = RunDAO().list() or []
|
|
67
70
|
|
|
68
|
-
result = [
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
result = [
|
|
72
|
+
{
|
|
73
|
+
"run_id": run.run_id,
|
|
74
|
+
"run_at": run.run_at,
|
|
75
|
+
"name": run.name,
|
|
76
|
+
"type": run.type,
|
|
77
|
+
"params": run.params,
|
|
78
|
+
"status": run.status,
|
|
79
|
+
"check_id": run.check_id,
|
|
80
|
+
}
|
|
81
|
+
for run in runs
|
|
82
|
+
]
|
|
77
83
|
|
|
78
84
|
# sort by run_at
|
|
79
|
-
result = sorted(result, key=lambda x: x[
|
|
85
|
+
result = sorted(result, key=lambda x: x["run_at"], reverse=True)
|
|
80
86
|
|
|
81
87
|
return result
|
|
82
88
|
|
|
@@ -101,7 +107,7 @@ async def search_runs_handler(search: SearchRunsIn):
|
|
|
101
107
|
result.append(run)
|
|
102
108
|
|
|
103
109
|
if search.limit:
|
|
104
|
-
return result[-search.limit:]
|
|
110
|
+
return result[-search.limit :]
|
|
105
111
|
|
|
106
112
|
return result
|
|
107
113
|
|
recce/apis/run_func.py
CHANGED
|
@@ -4,11 +4,11 @@ from typing import List, Optional
|
|
|
4
4
|
|
|
5
5
|
from recce.core import default_context
|
|
6
6
|
from recce.exceptions import RecceException
|
|
7
|
-
from recce.models import
|
|
7
|
+
from recce.models import Run, RunDAO, RunType
|
|
8
8
|
from recce.models.types import RunStatus
|
|
9
9
|
|
|
10
10
|
running_tasks = {}
|
|
11
|
-
logger = logging.getLogger(
|
|
11
|
+
logger = logging.getLogger("uvicorn")
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def _get_ref_model(sql_template: str) -> Optional[str]:
|
|
@@ -33,26 +33,26 @@ def generate_run_name(run):
|
|
|
33
33
|
now = dateutil.parser.parse(run.run_at)
|
|
34
34
|
|
|
35
35
|
if run_type == RunType.QUERY:
|
|
36
|
-
ref = _get_ref_model(params.get(
|
|
36
|
+
ref = _get_ref_model(params.get("sql_template"))
|
|
37
37
|
if ref:
|
|
38
38
|
return f"query of {ref}".capitalize()
|
|
39
39
|
return f"{'query'.capitalize()} - {now}"
|
|
40
40
|
elif run_type == RunType.QUERY_DIFF:
|
|
41
|
-
ref = _get_ref_model(params.get(
|
|
41
|
+
ref = _get_ref_model(params.get("sql_template"))
|
|
42
42
|
if ref:
|
|
43
43
|
return f"query diff of {ref}".capitalize()
|
|
44
44
|
return f"{'query diff'.capitalize()} - {now}"
|
|
45
45
|
elif run_type == RunType.VALUE_DIFF:
|
|
46
|
-
model = params.get(
|
|
46
|
+
model = params.get("model")
|
|
47
47
|
return f"value diff of {model}".capitalize()
|
|
48
48
|
elif run_type == RunType.VALUE_DIFF_DETAIL:
|
|
49
|
-
model = params.get(
|
|
49
|
+
model = params.get("model")
|
|
50
50
|
return f"value diff detail of {model}".capitalize()
|
|
51
51
|
elif run_type == RunType.PROFILE_DIFF:
|
|
52
|
-
model = params.get(
|
|
52
|
+
model = params.get("model")
|
|
53
53
|
return f"profile diff of {model}".capitalize()
|
|
54
54
|
elif run_type == RunType.ROW_COUNT_DIFF:
|
|
55
|
-
nodes = params.get(
|
|
55
|
+
nodes = params.get("node_names")
|
|
56
56
|
if nodes:
|
|
57
57
|
if len(nodes) == 1:
|
|
58
58
|
node = nodes[0]
|
|
@@ -62,23 +62,27 @@ def generate_run_name(run):
|
|
|
62
62
|
else:
|
|
63
63
|
return "row count of multiple nodes".capitalize()
|
|
64
64
|
elif run_type == RunType.TOP_K_DIFF:
|
|
65
|
-
model = params.get(
|
|
66
|
-
column = params.get(
|
|
65
|
+
model = params.get("model")
|
|
66
|
+
column = params.get("column_name")
|
|
67
67
|
return f"top-k diff of {model}.{column} ".capitalize()
|
|
68
68
|
elif run_type == RunType.HISTOGRAM_DIFF:
|
|
69
|
-
model = params.get(
|
|
70
|
-
column = params.get(
|
|
69
|
+
model = params.get("model")
|
|
70
|
+
column = params.get("column_name")
|
|
71
71
|
return f"histogram diff of {model}.{column} ".capitalize()
|
|
72
72
|
else:
|
|
73
73
|
return f"{'run'.capitalize()} - {now}"
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def create_task(run_type: RunType, params: dict):
|
|
77
|
-
if default_context().adapter_type ==
|
|
78
|
-
from recce.adapter.sqlmesh_adapter import
|
|
77
|
+
if default_context().adapter_type == "sqlmesh":
|
|
78
|
+
from recce.adapter.sqlmesh_adapter import (
|
|
79
|
+
sqlmesh_supported_registry as sqlmesh_registry,
|
|
80
|
+
)
|
|
81
|
+
|
|
79
82
|
registry = sqlmesh_registry
|
|
80
83
|
else:
|
|
81
84
|
from recce.adapter.dbt_adapter import dbt_supported_registry as dbt_registry
|
|
85
|
+
|
|
82
86
|
registry = dbt_registry
|
|
83
87
|
|
|
84
88
|
taskClz = registry.get(run_type)
|
|
@@ -101,6 +105,7 @@ def submit_run(type, params, check_id=None):
|
|
|
101
105
|
context = default_context()
|
|
102
106
|
if context.review_mode is True:
|
|
103
107
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
108
|
+
|
|
104
109
|
dbt_adaptor: DbtAdapter = context.adapter
|
|
105
110
|
if dbt_adaptor.adapter is None:
|
|
106
111
|
raise RecceException("Recce Server is not launched under DBT project folder.")
|
|
@@ -113,7 +118,7 @@ def submit_run(type, params, check_id=None):
|
|
|
113
118
|
running_tasks[run.run_id] = task
|
|
114
119
|
|
|
115
120
|
def progress_listener(message=None, percentage=None):
|
|
116
|
-
run.progress = {
|
|
121
|
+
run.progress = {"message": message, "percentage": percentage}
|
|
117
122
|
|
|
118
123
|
task.progress_listener = progress_listener
|
|
119
124
|
|
|
@@ -124,7 +129,7 @@ def submit_run(type, params, check_id=None):
|
|
|
124
129
|
run.result = result
|
|
125
130
|
run.status = RunStatus.FINISHED
|
|
126
131
|
if error is not None:
|
|
127
|
-
failed_reason = str(error) if str(error) !=
|
|
132
|
+
failed_reason = str(error) if str(error) != "None" else repr(error)
|
|
128
133
|
run.error = failed_reason
|
|
129
134
|
if run.status != RunStatus.CANCELLED:
|
|
130
135
|
run.status = RunStatus.FAILED
|
|
@@ -140,9 +145,10 @@ def submit_run(type, params, check_id=None):
|
|
|
140
145
|
if isinstance(e, RecceException) and e.is_raise is False:
|
|
141
146
|
return None
|
|
142
147
|
import sentry_sdk
|
|
148
|
+
|
|
143
149
|
sentry_sdk.capture_exception(e)
|
|
144
|
-
failed_reason = str(e) if str(e) !=
|
|
145
|
-
failed_reason = failed_reason.replace(
|
|
150
|
+
failed_reason = str(e) if str(e) != "None" else repr(e)
|
|
151
|
+
failed_reason = failed_reason.replace(". ", ".\n")
|
|
146
152
|
logger.error(f"Failed to execute {run_type} task: {failed_reason}")
|
|
147
153
|
return None
|
|
148
154
|
|
|
@@ -164,7 +170,7 @@ def cancel_run(run_id):
|
|
|
164
170
|
|
|
165
171
|
|
|
166
172
|
def materialize_run_results(runs: List[Run], nodes: List[str] = None):
|
|
167
|
-
|
|
173
|
+
"""
|
|
168
174
|
Materialize the run results for nodes. It walks through all runs and get the last results for primary run types.
|
|
169
175
|
|
|
170
176
|
The result format
|
|
@@ -180,11 +186,11 @@ def materialize_run_results(runs: List[Run], nodes: List[str] = None):
|
|
|
180
186
|
},
|
|
181
187
|
},
|
|
182
188
|
}
|
|
183
|
-
|
|
189
|
+
"""
|
|
184
190
|
|
|
185
191
|
context = default_context()
|
|
186
192
|
if context:
|
|
187
|
-
mame_to_unique_id = context.build_name_to_unique_id_index(excluded_types={
|
|
193
|
+
mame_to_unique_id = context.build_name_to_unique_id_index(excluded_types={"semantic_model", "metric"})
|
|
188
194
|
else:
|
|
189
195
|
mame_to_unique_id = {}
|
|
190
196
|
|
|
@@ -205,7 +211,7 @@ def materialize_run_results(runs: List[Run], nodes: List[str] = None):
|
|
|
205
211
|
node_result = result[key] = {}
|
|
206
212
|
else:
|
|
207
213
|
node_result = result.get(key)
|
|
208
|
-
node_result[
|
|
214
|
+
node_result["row_count_diff"] = {"run_id": run.run_id, "result": node_run_result}
|
|
209
215
|
elif run.type == RunType.ROW_COUNT:
|
|
210
216
|
for model_name, node_run_result in run.result.items():
|
|
211
217
|
key = mame_to_unique_id.get(model_name, model_name)
|
|
@@ -218,5 +224,5 @@ def materialize_run_results(runs: List[Run], nodes: List[str] = None):
|
|
|
218
224
|
node_result = result[key] = {}
|
|
219
225
|
else:
|
|
220
226
|
node_result = result.get(key)
|
|
221
|
-
node_result[
|
|
227
|
+
node_result["row_count"] = {"run_id": run.run_id, "result": node_run_result}
|
|
222
228
|
return result
|