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
recce/config.py
CHANGED
|
@@ -5,11 +5,11 @@ from recce import yaml
|
|
|
5
5
|
from recce.exceptions import RecceConfigException
|
|
6
6
|
from recce.util import SingletonMeta
|
|
7
7
|
|
|
8
|
-
RECCE_CONFIG_FILE =
|
|
9
|
-
RECCE_PRESET_CHECK_COMMENT =
|
|
8
|
+
RECCE_CONFIG_FILE = "recce.yml"
|
|
9
|
+
RECCE_PRESET_CHECK_COMMENT = """Preset Checks
|
|
10
10
|
Please see https://docs.datarecce.io/features/preset-checks/
|
|
11
|
-
|
|
12
|
-
RECCE_ERROR_LOG_FILE =
|
|
11
|
+
"""
|
|
12
|
+
RECCE_ERROR_LOG_FILE = "recce_error.log"
|
|
13
13
|
console = Console()
|
|
14
14
|
|
|
15
15
|
|
|
@@ -21,83 +21,92 @@ class RecceConfig(metaclass=SingletonMeta):
|
|
|
21
21
|
|
|
22
22
|
def load(self):
|
|
23
23
|
try:
|
|
24
|
-
with open(self.config_file,
|
|
24
|
+
with open(self.config_file, "r", encoding="utf-8") as f:
|
|
25
25
|
config = yaml.safe_load(f)
|
|
26
26
|
self.config = config if config else {}
|
|
27
27
|
self._verify_preset_checks()
|
|
28
28
|
except FileNotFoundError:
|
|
29
|
-
console.print(f
|
|
29
|
+
console.print(f"[[orange3]NOTICE[/orange3]] Generate default Recce config file at '{self.config_file}'")
|
|
30
30
|
self.config = self.generate_template()
|
|
31
31
|
self.save()
|
|
32
32
|
|
|
33
33
|
def _verify_preset_checks(self):
|
|
34
34
|
from recce.tasks.core import CheckValidator
|
|
35
35
|
|
|
36
|
-
if not self.config.get(
|
|
36
|
+
if not self.config.get("checks"):
|
|
37
37
|
return
|
|
38
38
|
|
|
39
|
-
for check in self.config[
|
|
39
|
+
for check in self.config["checks"]:
|
|
40
40
|
try:
|
|
41
|
-
check_type = check.get(
|
|
41
|
+
check_type = check.get("type")
|
|
42
42
|
if check_type is None:
|
|
43
43
|
raise ValueError(f'Check type is required for check "{check}"')
|
|
44
|
-
if check_type ==
|
|
44
|
+
if check_type == "lineage_diff":
|
|
45
45
|
from recce.tasks.lineage import LineageDiffCheckValidator
|
|
46
|
+
|
|
46
47
|
validator = LineageDiffCheckValidator()
|
|
47
|
-
elif check_type ==
|
|
48
|
+
elif check_type == "schema_diff":
|
|
48
49
|
from recce.tasks.schema import SchemaDiffCheckValidator
|
|
50
|
+
|
|
49
51
|
validator = SchemaDiffCheckValidator()
|
|
50
|
-
elif check_type ==
|
|
52
|
+
elif check_type == "row_count_diff":
|
|
51
53
|
from recce.tasks.rowcount import RowCountDiffCheckValidator
|
|
54
|
+
|
|
52
55
|
validator = RowCountDiffCheckValidator()
|
|
53
|
-
elif check_type ==
|
|
56
|
+
elif check_type == "query":
|
|
54
57
|
from recce.tasks.query import QueryCheckValidator
|
|
58
|
+
|
|
55
59
|
validator = QueryCheckValidator()
|
|
56
|
-
elif check_type ==
|
|
60
|
+
elif check_type == "query_diff":
|
|
57
61
|
from recce.tasks.query import QueryDiffCheckValidator
|
|
62
|
+
|
|
58
63
|
validator = QueryDiffCheckValidator()
|
|
59
|
-
elif check_type ==
|
|
64
|
+
elif check_type == "value_diff" or check_type == "value_diff_detail":
|
|
60
65
|
from recce.tasks.valuediff import ValueDiffCheckValidator
|
|
66
|
+
|
|
61
67
|
validator = ValueDiffCheckValidator()
|
|
62
|
-
elif check_type ==
|
|
68
|
+
elif check_type == "profile_diff":
|
|
63
69
|
from recce.tasks.profile import ProfileCheckValidator
|
|
70
|
+
|
|
64
71
|
validator = ProfileCheckValidator()
|
|
65
|
-
elif check_type ==
|
|
72
|
+
elif check_type == "top_k_diff":
|
|
66
73
|
from recce.tasks.top_k import TopKDiffCheckValidator
|
|
74
|
+
|
|
67
75
|
validator = TopKDiffCheckValidator()
|
|
68
|
-
elif check_type ==
|
|
76
|
+
elif check_type == "histogram_diff":
|
|
69
77
|
from recce.tasks.histogram import HistogramDiffCheckValidator
|
|
78
|
+
|
|
70
79
|
validator = HistogramDiffCheckValidator()
|
|
71
80
|
else:
|
|
72
81
|
validator = CheckValidator()
|
|
73
82
|
validator.validate(check)
|
|
74
83
|
except Exception as e:
|
|
75
84
|
import json
|
|
85
|
+
|
|
76
86
|
raise RecceConfigException(
|
|
77
|
-
f"Load preset checks failed from '{self.config_file}'\n{json.dumps(check, indent=2)}",
|
|
78
|
-
|
|
87
|
+
f"Load preset checks failed from '{self.config_file}'\n{json.dumps(check, indent=2)}", cause=e
|
|
88
|
+
)
|
|
79
89
|
|
|
80
90
|
def generate_template(self):
|
|
81
|
-
data = yaml.CommentedMap(
|
|
82
|
-
|
|
83
|
-
data.yaml_set_comment_before_after_key('checks', before=RECCE_PRESET_CHECK_COMMENT)
|
|
91
|
+
data = yaml.CommentedMap(checks=yaml.CommentedSeq())
|
|
92
|
+
data.yaml_set_comment_before_after_key("checks", before=RECCE_PRESET_CHECK_COMMENT)
|
|
84
93
|
# Define default preset checks
|
|
85
94
|
default_checks = [
|
|
86
95
|
yaml.CommentedMap(
|
|
87
|
-
name=
|
|
88
|
-
description=
|
|
89
|
-
type=
|
|
90
|
-
params={
|
|
96
|
+
name="Row count diff",
|
|
97
|
+
description="Check the row count diff for all table models.",
|
|
98
|
+
type="row_count_diff",
|
|
99
|
+
params={"select": "state:modified,config.materialized:table"},
|
|
91
100
|
),
|
|
92
101
|
yaml.CommentedMap(
|
|
93
|
-
name=
|
|
94
|
-
description=
|
|
95
|
-
type=
|
|
96
|
-
)
|
|
102
|
+
name="Schema diff",
|
|
103
|
+
description="Check the schema diff for all nodes.",
|
|
104
|
+
type="schema_diff",
|
|
105
|
+
),
|
|
97
106
|
]
|
|
98
107
|
|
|
99
108
|
for check in default_checks:
|
|
100
|
-
data[
|
|
109
|
+
data["checks"].append(check)
|
|
101
110
|
|
|
102
111
|
return data
|
|
103
112
|
|
|
@@ -108,7 +117,7 @@ class RecceConfig(metaclass=SingletonMeta):
|
|
|
108
117
|
self.config[key] = value
|
|
109
118
|
|
|
110
119
|
def save(self):
|
|
111
|
-
with open(RECCE_CONFIG_FILE,
|
|
120
|
+
with open(RECCE_CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
112
121
|
yaml.dump(self.config, f)
|
|
113
122
|
|
|
114
123
|
def __str__(self):
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os.path
|
|
3
|
+
import random
|
|
4
|
+
import threading
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Tuple
|
|
8
|
+
from urllib.parse import parse_qs, urlparse
|
|
9
|
+
|
|
10
|
+
from cryptography.hazmat.backends import default_backend
|
|
11
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
12
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
13
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from recce.event import update_recce_api_token
|
|
17
|
+
from recce.exceptions import RecceConfigException
|
|
18
|
+
from recce.util.onboarding_state import update_onboarding_state
|
|
19
|
+
from recce.util.recce_cloud import RECCE_CLOUD_BASE_URL, RecceCloud
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
static_folder_path = Path(__file__).parent / "data"
|
|
24
|
+
_server_lock = threading.Lock()
|
|
25
|
+
_connection_url = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def decrypt_code(private_key: RSAPrivateKey, code: str) -> str:
|
|
29
|
+
ciphertext = base64.b64decode(code)
|
|
30
|
+
plaintext = private_key.decrypt(
|
|
31
|
+
ciphertext,
|
|
32
|
+
padding.OAEP(
|
|
33
|
+
mgf=padding.MGF1(algorithm=hashes.SHA1()), # Node.js uses SHA1 by default
|
|
34
|
+
algorithm=hashes.SHA1(),
|
|
35
|
+
label=None,
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
return plaintext.decode("utf-8")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def handle_callback_request(query_string: str, private_key: RSAPrivateKey):
|
|
42
|
+
query_params = parse_qs(query_string)
|
|
43
|
+
code = query_params.get("code", [None])[0]
|
|
44
|
+
if not code:
|
|
45
|
+
raise RecceConfigException("Missing `code` in query")
|
|
46
|
+
|
|
47
|
+
api_token = decrypt_code(private_key, code)
|
|
48
|
+
if not RecceCloud(api_token).verify_token():
|
|
49
|
+
raise RecceConfigException("Invalid Recce Cloud API token")
|
|
50
|
+
|
|
51
|
+
update_recce_api_token(api_token)
|
|
52
|
+
update_onboarding_state(api_token, False)
|
|
53
|
+
|
|
54
|
+
return api_token # for testability/debugging
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def make_callback_handler(private_key: RSAPrivateKey):
|
|
58
|
+
class OneTimeHTTPRequestHandler(BaseHTTPRequestHandler):
|
|
59
|
+
def do_GET(self):
|
|
60
|
+
try:
|
|
61
|
+
with open(os.path.join(static_folder_path, "auth_callback.html"), "r", encoding="utf-8") as f:
|
|
62
|
+
callback_html_content = f.read()
|
|
63
|
+
|
|
64
|
+
# Parse query parameters
|
|
65
|
+
parsed_url = urlparse(self.path)
|
|
66
|
+
|
|
67
|
+
handle_callback_request(parsed_url.query, private_key)
|
|
68
|
+
|
|
69
|
+
# Construct HTML content
|
|
70
|
+
self.send_response(200)
|
|
71
|
+
self.send_header("Content-Type", "text/html")
|
|
72
|
+
self.send_header("Content-Length", str(len(callback_html_content.encode())))
|
|
73
|
+
self.end_headers()
|
|
74
|
+
self.wfile.write(callback_html_content.encode())
|
|
75
|
+
|
|
76
|
+
except Exception:
|
|
77
|
+
console.print_exception()
|
|
78
|
+
self.send_response(500)
|
|
79
|
+
self.end_headers()
|
|
80
|
+
self.wfile.write(b"<h1>Internal Server Error</h1>")
|
|
81
|
+
finally:
|
|
82
|
+
# Shut down the server after handling the first request
|
|
83
|
+
# Shutdown in a new thread to avoid deadlock
|
|
84
|
+
self.server.server_close()
|
|
85
|
+
threading.Thread(target=self.server.shutdown, daemon=True).start()
|
|
86
|
+
|
|
87
|
+
def log_message(self, format, *args):
|
|
88
|
+
# Suppress default logging
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
return OneTimeHTTPRequestHandler
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def is_callback_server_running():
|
|
95
|
+
return _server_lock.locked()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_connection_url():
|
|
99
|
+
return _connection_url
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_one_time_http_server(private_key: RSAPrivateKey, port=8080):
|
|
103
|
+
handler = make_callback_handler(private_key)
|
|
104
|
+
server = HTTPServer(("localhost", port), handler)
|
|
105
|
+
server.serve_forever()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def prepare_connection_url(public_key: RSAPublicKey):
|
|
109
|
+
public_key_pem_bytes = public_key.public_bytes(
|
|
110
|
+
encoding=serialization.Encoding.PEM,
|
|
111
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
112
|
+
)
|
|
113
|
+
public_key_pem_str = base64.b64encode(public_key_pem_bytes).decode("utf-8")
|
|
114
|
+
callback_port = random.randint(10000, 15000)
|
|
115
|
+
connect_url = f"{RECCE_CLOUD_BASE_URL}/connect?_key={public_key_pem_str}&_port={callback_port}"
|
|
116
|
+
return connect_url, callback_port
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def generate_key_pair() -> Tuple[RSAPrivateKey, RSAPublicKey]:
|
|
120
|
+
key_size = 2048 # Should be at least 2048
|
|
121
|
+
|
|
122
|
+
private_key = rsa.generate_private_key(
|
|
123
|
+
public_exponent=65537, key_size=key_size, backend=default_backend() # Do not change
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
public_key = private_key.public_key()
|
|
127
|
+
return private_key, public_key
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def connect_to_cloud_background_task(private_key: RSAPrivateKey, callback_port, connection_url):
|
|
131
|
+
if is_callback_server_running():
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
with _server_lock:
|
|
135
|
+
global _connection_url
|
|
136
|
+
_connection_url = connection_url
|
|
137
|
+
run_one_time_http_server(private_key, callback_port)
|
|
138
|
+
_connection_url = None
|
recce/core.py
CHANGED
|
@@ -3,15 +3,21 @@ import json
|
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
|
-
from typing import Callable, Dict, Optional,
|
|
6
|
+
from typing import Callable, Dict, List, Optional, Set, Tuple
|
|
7
7
|
|
|
8
8
|
from recce.adapter.base import BaseAdapter
|
|
9
9
|
from recce.models import Check, Run
|
|
10
10
|
from recce.models.types import LineageDiff
|
|
11
|
-
from recce.state import
|
|
11
|
+
from recce.state import (
|
|
12
|
+
GitRepoInfo,
|
|
13
|
+
PullRequestInfo,
|
|
14
|
+
RecceState,
|
|
15
|
+
RecceStateLoader,
|
|
16
|
+
RecceStateMetadata,
|
|
17
|
+
)
|
|
12
18
|
from recce.util.recce_cloud import set_recce_cloud_onboarding_state
|
|
13
19
|
|
|
14
|
-
logger = logging.getLogger(
|
|
20
|
+
logger = logging.getLogger("uvicorn")
|
|
15
21
|
|
|
16
22
|
|
|
17
23
|
@dataclass
|
|
@@ -25,8 +31,8 @@ class RecceContext:
|
|
|
25
31
|
|
|
26
32
|
@classmethod
|
|
27
33
|
def load(cls, **kwargs):
|
|
28
|
-
state_loader: RecceStateLoader = kwargs.get(
|
|
29
|
-
is_review_mode = kwargs.get(
|
|
34
|
+
state_loader: RecceStateLoader = kwargs.get("state_loader")
|
|
35
|
+
is_review_mode = kwargs.get("review", False)
|
|
30
36
|
|
|
31
37
|
context = cls(
|
|
32
38
|
review_mode=is_review_mode,
|
|
@@ -34,14 +40,16 @@ class RecceContext:
|
|
|
34
40
|
)
|
|
35
41
|
|
|
36
42
|
# Initiate the adapter
|
|
37
|
-
if kwargs.get(
|
|
38
|
-
logger.warning(
|
|
43
|
+
if kwargs.get("sqlmesh", False):
|
|
44
|
+
logger.warning("SQLMesh adapter is still in EXPERIMENTAL mode.")
|
|
39
45
|
from recce.adapter.sqlmesh_adapter import SqlmeshAdapter
|
|
40
|
-
|
|
46
|
+
|
|
47
|
+
context.adapter_type = "sqlmesh"
|
|
41
48
|
context.adapter = SqlmeshAdapter.load(**kwargs)
|
|
42
49
|
else:
|
|
43
50
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
44
|
-
|
|
51
|
+
|
|
52
|
+
context.adapter_type = "dbt"
|
|
45
53
|
context.adapter = DbtAdapter.load(**kwargs)
|
|
46
54
|
|
|
47
55
|
# Import state
|
|
@@ -52,7 +60,7 @@ class RecceContext:
|
|
|
52
60
|
|
|
53
61
|
if is_review_mode:
|
|
54
62
|
if not state:
|
|
55
|
-
raise Exception(
|
|
63
|
+
raise Exception("The state file is required for review mode")
|
|
56
64
|
|
|
57
65
|
return context
|
|
58
66
|
|
|
@@ -76,14 +84,14 @@ class RecceContext:
|
|
|
76
84
|
curr = self.get_lineage(base=False)
|
|
77
85
|
base = self.get_lineage(base=True)
|
|
78
86
|
|
|
79
|
-
for unique_id, node in curr[
|
|
80
|
-
if excluded_types and node.get(
|
|
87
|
+
for unique_id, node in curr["nodes"].items():
|
|
88
|
+
if excluded_types and node.get("resource_type") in excluded_types:
|
|
81
89
|
continue
|
|
82
|
-
name_to_unique_id[node[
|
|
83
|
-
for unique_id, node in base[
|
|
84
|
-
if excluded_types and node.get(
|
|
90
|
+
name_to_unique_id[node["name"]] = unique_id
|
|
91
|
+
for unique_id, node in base["nodes"].items():
|
|
92
|
+
if excluded_types and node.get("resource_type") in excluded_types:
|
|
85
93
|
continue
|
|
86
|
-
name_to_unique_id[node[
|
|
94
|
+
name_to_unique_id[node["name"]] = unique_id
|
|
87
95
|
return name_to_unique_id
|
|
88
96
|
|
|
89
97
|
def start_monitor_artifacts(self, callback: Callable = None):
|
|
@@ -118,7 +126,7 @@ class RecceContext:
|
|
|
118
126
|
state.git = self.state_loader.state.git
|
|
119
127
|
state.pull_request = self.state_loader.state.pull_request
|
|
120
128
|
else:
|
|
121
|
-
git = GitRepoInfo.
|
|
129
|
+
git = GitRepoInfo.from_current_repository()
|
|
122
130
|
if git:
|
|
123
131
|
state.git = git
|
|
124
132
|
if self.state_loader.pr_info:
|
|
@@ -137,10 +145,10 @@ class RecceContext:
|
|
|
137
145
|
state.runs = self.runs
|
|
138
146
|
state.checks = self.checks
|
|
139
147
|
state.artifacts = self.adapter.export_artifacts()
|
|
140
|
-
git = GitRepoInfo.
|
|
148
|
+
git = GitRepoInfo.from_current_repository()
|
|
141
149
|
if git:
|
|
142
150
|
state.git = git
|
|
143
|
-
pr = PullRequestInfo(url=os.getenv(
|
|
151
|
+
pr = PullRequestInfo(url=os.getenv("RECCE_PR_URL"))
|
|
144
152
|
state.pull_request = pr
|
|
145
153
|
|
|
146
154
|
return state
|
|
@@ -152,38 +160,37 @@ class RecceContext:
|
|
|
152
160
|
:param method: merge, revert, overwrite
|
|
153
161
|
|
|
154
162
|
"""
|
|
155
|
-
if method ==
|
|
163
|
+
if method == "merge":
|
|
156
164
|
self.state_loader.refresh()
|
|
157
165
|
self.import_state(self.state_loader.state, merge=True)
|
|
158
166
|
state = self.export_state()
|
|
159
167
|
self.state_loader.export(state)
|
|
160
|
-
elif method ==
|
|
168
|
+
elif method == "revert":
|
|
161
169
|
self.state_loader.refresh()
|
|
162
170
|
self.import_state(self.state_loader.state, merge=False)
|
|
163
|
-
elif method ==
|
|
171
|
+
elif method == "overwrite":
|
|
164
172
|
state = self.export_state()
|
|
165
173
|
self.state_loader.export(state)
|
|
166
174
|
else:
|
|
167
|
-
raise Exception(f
|
|
175
|
+
raise Exception(f"Unsupported method: {method}")
|
|
168
176
|
|
|
169
177
|
def _merge_checks(self, import_checks: list[Check]):
|
|
170
178
|
checks = list(self.checks)
|
|
171
179
|
imports = 0
|
|
172
180
|
|
|
173
181
|
def _calculate_checksum(c: Check):
|
|
174
|
-
payload = json.dumps(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
payload = json.dumps(
|
|
183
|
+
{
|
|
184
|
+
"type": str(c.type),
|
|
185
|
+
"params": c.params,
|
|
186
|
+
"view_options": c.view_options,
|
|
187
|
+
},
|
|
188
|
+
sort_keys=True,
|
|
189
|
+
)
|
|
179
190
|
return hashlib.sha256(payload.encode()).hexdigest()
|
|
180
191
|
|
|
181
|
-
checksum_map = {
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
check_map = {
|
|
185
|
-
c.check_id: c for c in self.checks
|
|
186
|
-
}
|
|
192
|
+
checksum_map = {_calculate_checksum(c): c for c in self.checks if c.is_preset}
|
|
193
|
+
check_map = {c.check_id: c for c in self.checks}
|
|
187
194
|
|
|
188
195
|
# merge checks
|
|
189
196
|
for imported in import_checks:
|
|
@@ -219,12 +226,12 @@ class RecceContext:
|
|
|
219
226
|
return imports
|
|
220
227
|
|
|
221
228
|
def import_state(self, import_state: RecceState, merge: bool = True):
|
|
222
|
-
|
|
229
|
+
"""
|
|
223
230
|
Import the state from another RecceState object.
|
|
224
231
|
|
|
225
232
|
:param import_state: the state to import
|
|
226
233
|
:param merge: whether to merge the state or replace the current state
|
|
227
|
-
|
|
234
|
+
"""
|
|
228
235
|
import_runs = 0
|
|
229
236
|
import_checks = 0
|
|
230
237
|
if merge:
|
|
@@ -261,24 +268,25 @@ class RecceContext:
|
|
|
261
268
|
def mark_onboarding_completed(self):
|
|
262
269
|
if self.state_loader.cloud_mode:
|
|
263
270
|
try:
|
|
264
|
-
token = self.state_loader.cloud_options.get(
|
|
265
|
-
set_recce_cloud_onboarding_state(token,
|
|
271
|
+
token = self.state_loader.cloud_options.get("github_token")
|
|
272
|
+
set_recce_cloud_onboarding_state(token, "completed")
|
|
266
273
|
except Exception as e:
|
|
267
|
-
logger.debug(f
|
|
274
|
+
logger.debug(f"Failed to mark onboarding completed in Recce Cloud. Reason: {str(e)}")
|
|
268
275
|
else:
|
|
269
276
|
# Skip the onboarding state for non-cloud mode
|
|
270
277
|
pass
|
|
271
278
|
|
|
272
279
|
@staticmethod
|
|
273
280
|
def verify_required_artifacts(**kwargs) -> Tuple[bool, Optional[str]]:
|
|
274
|
-
if kwargs.get(
|
|
281
|
+
if kwargs.get("sqlmesh", False):
|
|
275
282
|
pass
|
|
276
283
|
else:
|
|
277
284
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
285
|
+
|
|
278
286
|
try:
|
|
279
287
|
DbtAdapter.load(**kwargs)
|
|
280
288
|
except FileNotFoundError as e:
|
|
281
|
-
return False, f"Cannot load the manifest: '{e.filename}'"
|
|
289
|
+
return False, f"Cannot load the manifest: '{e.filename}'. Type 'recce debug'."
|
|
282
290
|
|
|
283
291
|
return True, None
|
|
284
292
|
|
|
@@ -289,18 +297,18 @@ class RecceContext:
|
|
|
289
297
|
"""
|
|
290
298
|
The state loader mode is used for telemetry purpose.
|
|
291
299
|
"""
|
|
292
|
-
if os.environ.get(
|
|
293
|
-
return
|
|
300
|
+
if os.environ.get("DEMO", False):
|
|
301
|
+
return "demo"
|
|
294
302
|
|
|
295
303
|
if not self.state_loader:
|
|
296
|
-
return
|
|
304
|
+
return "none"
|
|
297
305
|
|
|
298
306
|
if self.state_loader.cloud_mode:
|
|
299
|
-
return
|
|
307
|
+
return "cloud"
|
|
300
308
|
elif self.state_loader.state_file:
|
|
301
|
-
return
|
|
309
|
+
return "file"
|
|
302
310
|
else:
|
|
303
|
-
return
|
|
311
|
+
return "none"
|
|
304
312
|
|
|
305
313
|
|
|
306
314
|
recce_context: Optional[RecceContext] = None
|