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.

Files changed (213) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +27 -22
  3. recce/adapter/base.py +11 -14
  4. recce/adapter/dbt_adapter/__init__.py +810 -480
  5. recce/adapter/dbt_adapter/dbt_version.py +3 -0
  6. recce/adapter/sqlmesh_adapter.py +24 -35
  7. recce/apis/check_api.py +39 -28
  8. recce/apis/check_func.py +33 -27
  9. recce/apis/run_api.py +25 -19
  10. recce/apis/run_func.py +29 -23
  11. recce/artifact.py +119 -51
  12. recce/cli.py +1299 -323
  13. recce/config.py +42 -33
  14. recce/connect_to_cloud.py +138 -0
  15. recce/core.py +55 -47
  16. recce/data/404.html +1 -1
  17. recce/data/__next.__PAGE__.txt +10 -0
  18. recce/data/__next._full.txt +23 -0
  19. recce/data/__next._head.txt +8 -0
  20. recce/data/__next._index.txt +8 -0
  21. recce/data/__next._tree.txt +5 -0
  22. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
  23. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
  24. recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
  25. recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
  26. recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
  27. recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
  28. recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
  29. recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
  30. recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
  31. recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
  32. recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
  33. recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
  34. recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
  35. recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
  36. recce/data/_next/static/chunks/99d638224186c118.js +1 -0
  37. recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
  38. recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
  39. recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
  40. recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
  41. recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
  42. recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
  43. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  44. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  45. recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
  46. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  47. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
  48. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  49. recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
  50. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  51. recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
  52. recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
  53. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  54. recce/data/_next/static/media/reload-image.7aa931c7.svg +4 -0
  55. recce/data/_not-found/__next._full.txt +17 -0
  56. recce/data/_not-found/__next._head.txt +8 -0
  57. recce/data/_not-found/__next._index.txt +8 -0
  58. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  59. recce/data/_not-found/__next._not-found.txt +4 -0
  60. recce/data/_not-found/__next._tree.txt +3 -0
  61. recce/data/_not-found.html +1 -0
  62. recce/data/_not-found.txt +17 -0
  63. recce/data/auth_callback.html +68 -0
  64. recce/data/imgs/reload-image.svg +4 -0
  65. recce/data/index.html +1 -27
  66. recce/data/index.txt +23 -7
  67. recce/diff.py +6 -12
  68. recce/event/__init__.py +86 -74
  69. recce/event/collector.py +33 -22
  70. recce/event/track.py +49 -27
  71. recce/exceptions.py +1 -1
  72. recce/git.py +7 -7
  73. recce/github.py +57 -53
  74. recce/mcp_server.py +716 -0
  75. recce/models/__init__.py +4 -1
  76. recce/models/check.py +6 -7
  77. recce/models/run.py +1 -0
  78. recce/models/types.py +131 -28
  79. recce/pull_request.py +27 -25
  80. recce/run.py +165 -121
  81. recce/server.py +303 -111
  82. recce/state/__init__.py +31 -0
  83. recce/state/cloud.py +632 -0
  84. recce/state/const.py +26 -0
  85. recce/state/local.py +56 -0
  86. recce/state/state.py +119 -0
  87. recce/state/state_loader.py +174 -0
  88. recce/summary.py +188 -143
  89. recce/tasks/__init__.py +19 -3
  90. recce/tasks/core.py +11 -13
  91. recce/tasks/dataframe.py +82 -18
  92. recce/tasks/histogram.py +69 -34
  93. recce/tasks/lineage.py +2 -2
  94. recce/tasks/profile.py +152 -86
  95. recce/tasks/query.py +139 -87
  96. recce/tasks/rowcount.py +37 -31
  97. recce/tasks/schema.py +18 -15
  98. recce/tasks/top_k.py +35 -35
  99. recce/tasks/valuediff.py +216 -152
  100. recce/util/__init__.py +3 -0
  101. recce/util/api_token.py +80 -0
  102. recce/util/breaking.py +87 -85
  103. recce/util/cll.py +274 -219
  104. recce/util/io.py +22 -17
  105. recce/util/lineage.py +65 -16
  106. recce/util/logger.py +1 -1
  107. recce/util/onboarding_state.py +45 -0
  108. recce/util/perf_tracking.py +85 -0
  109. recce/util/recce_cloud.py +322 -72
  110. recce/util/singleton.py +4 -4
  111. recce/yaml/__init__.py +7 -10
  112. recce_cloud/__init__.py +24 -0
  113. recce_cloud/api/__init__.py +17 -0
  114. recce_cloud/api/base.py +111 -0
  115. recce_cloud/api/client.py +150 -0
  116. recce_cloud/api/exceptions.py +26 -0
  117. recce_cloud/api/factory.py +63 -0
  118. recce_cloud/api/github.py +76 -0
  119. recce_cloud/api/gitlab.py +82 -0
  120. recce_cloud/artifact.py +57 -0
  121. recce_cloud/ci_providers/__init__.py +9 -0
  122. recce_cloud/ci_providers/base.py +82 -0
  123. recce_cloud/ci_providers/detector.py +147 -0
  124. recce_cloud/ci_providers/github_actions.py +136 -0
  125. recce_cloud/ci_providers/gitlab_ci.py +130 -0
  126. recce_cloud/cli.py +245 -0
  127. recce_cloud/upload.py +214 -0
  128. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
  129. recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
  130. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
  131. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
  132. tests/adapter/dbt_adapter/conftest.py +9 -5
  133. tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
  134. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
  135. tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
  136. tests/adapter/dbt_adapter/test_selector.py +22 -21
  137. tests/recce_cloud/__init__.py +0 -0
  138. tests/recce_cloud/test_ci_providers.py +351 -0
  139. tests/recce_cloud/test_cli.py +372 -0
  140. tests/recce_cloud/test_client.py +273 -0
  141. tests/recce_cloud/test_platform_clients.py +333 -0
  142. tests/tasks/conftest.py +1 -1
  143. tests/tasks/test_histogram.py +58 -66
  144. tests/tasks/test_lineage.py +36 -23
  145. tests/tasks/test_preset_checks.py +45 -31
  146. tests/tasks/test_profile.py +339 -15
  147. tests/tasks/test_query.py +46 -46
  148. tests/tasks/test_row_count.py +65 -46
  149. tests/tasks/test_schema.py +65 -42
  150. tests/tasks/test_top_k.py +22 -18
  151. tests/tasks/test_valuediff.py +43 -32
  152. tests/test_cli.py +174 -60
  153. tests/test_cli_mcp_optional.py +45 -0
  154. tests/test_cloud_listing_cli.py +324 -0
  155. tests/test_config.py +7 -9
  156. tests/test_connect_to_cloud.py +82 -0
  157. tests/test_core.py +151 -4
  158. tests/test_dbt.py +7 -7
  159. tests/test_mcp_server.py +332 -0
  160. tests/test_pull_request.py +1 -1
  161. tests/test_server.py +25 -19
  162. tests/test_summary.py +29 -17
  163. recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
  164. recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
  165. recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
  166. recce/data/_next/static/chunks/368-7587b306577df275.js +0 -65
  167. recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
  168. recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
  169. recce/data/_next/static/chunks/3a92ee20-3b5d922d4157af5e.js +0 -1
  170. recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
  171. recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
  172. recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
  173. recce/data/_next/static/chunks/6ef81909-694dc38134099299.js +0 -1
  174. recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
  175. recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
  176. recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
  177. recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
  178. recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
  179. recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
  180. recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
  181. recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
  182. recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
  183. recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
  184. recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
  185. recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
  186. recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
  187. recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
  188. recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
  189. recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
  190. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  191. recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
  192. recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
  193. recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
  194. recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
  195. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  196. recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
  197. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  198. recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
  199. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
  200. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  201. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  202. recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
  203. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  204. recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
  205. recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
  206. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  207. recce/state.py +0 -753
  208. recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
  209. tests/test_state.py +0 -123
  210. /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
  211. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  212. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
  213. {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 = 'recce.yml'
9
- RECCE_PRESET_CHECK_COMMENT = '''Preset Checks
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 = 'recce_error.log'
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, 'r') as f:
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'Recce config file not found. Generating default config file at \'{self.config_file}\'')
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('checks'):
36
+ if not self.config.get("checks"):
37
37
  return
38
38
 
39
- for check in self.config['checks']:
39
+ for check in self.config["checks"]:
40
40
  try:
41
- check_type = check.get('type')
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 == 'lineage_diff':
44
+ if check_type == "lineage_diff":
45
45
  from recce.tasks.lineage import LineageDiffCheckValidator
46
+
46
47
  validator = LineageDiffCheckValidator()
47
- elif check_type == 'schema_diff':
48
+ elif check_type == "schema_diff":
48
49
  from recce.tasks.schema import SchemaDiffCheckValidator
50
+
49
51
  validator = SchemaDiffCheckValidator()
50
- elif check_type == 'row_count_diff':
52
+ elif check_type == "row_count_diff":
51
53
  from recce.tasks.rowcount import RowCountDiffCheckValidator
54
+
52
55
  validator = RowCountDiffCheckValidator()
53
- elif check_type == 'query':
56
+ elif check_type == "query":
54
57
  from recce.tasks.query import QueryCheckValidator
58
+
55
59
  validator = QueryCheckValidator()
56
- elif check_type == 'query_diff':
60
+ elif check_type == "query_diff":
57
61
  from recce.tasks.query import QueryDiffCheckValidator
62
+
58
63
  validator = QueryDiffCheckValidator()
59
- elif check_type == 'value_diff' or check_type == 'value_diff_detail':
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 == 'profile_diff':
68
+ elif check_type == "profile_diff":
63
69
  from recce.tasks.profile import ProfileCheckValidator
70
+
64
71
  validator = ProfileCheckValidator()
65
- elif check_type == 'top_k_diff':
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 == 'histogram_diff':
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
- cause=e)
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
- checks=yaml.CommentedSeq())
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='Row count diff',
88
- description='Check the row count diff for all table models.',
89
- type='row_count_diff',
90
- params={'select': 'state:modified,config.materialized:table'},
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='Schema diff',
94
- description='Check the schema diff for all nodes.',
95
- type='schema_diff',
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['checks'].append(check)
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, 'w') as f:
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, List, Tuple, Set
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 RecceState, RecceStateMetadata, GitRepoInfo, PullRequestInfo, RecceStateLoader
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('uvicorn')
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('state_loader')
29
- is_review_mode = kwargs.get('review', False)
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('sqlmesh', False):
38
- logger.warning('SQLMesh adapter is still in EXPERIMENTAL mode.')
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
- context.adapter_type = 'sqlmesh'
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
- context.adapter_type = 'dbt'
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('The state file is required for review mode')
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['nodes'].items():
80
- if excluded_types and node.get('resource_type') in excluded_types:
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['name']] = unique_id
83
- for unique_id, node in base['nodes'].items():
84
- if excluded_types and node.get('resource_type') in excluded_types:
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['name']] = unique_id
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.from_current_repositroy()
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.from_current_repositroy()
148
+ git = GitRepoInfo.from_current_repository()
141
149
  if git:
142
150
  state.git = git
143
- pr = PullRequestInfo(url=os.getenv('RECCE_PR_URL'))
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 == 'merge':
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 == 'revert':
168
+ elif method == "revert":
161
169
  self.state_loader.refresh()
162
170
  self.import_state(self.state_loader.state, merge=False)
163
- elif method == 'overwrite':
171
+ elif method == "overwrite":
164
172
  state = self.export_state()
165
173
  self.state_loader.export(state)
166
174
  else:
167
- raise Exception(f'Unsupported method: {method}')
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
- 'type': str(c.type),
176
- 'params': c.params,
177
- 'view_options': c.view_options,
178
- }, sort_keys=True)
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
- _calculate_checksum(c): c for c in self.checks if c.is_preset
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('token')
265
- set_recce_cloud_onboarding_state(token, 'completed')
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'Failed to mark onboarding completed in Recce Cloud. Reason: {str(e)}')
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('sqlmesh', False):
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('DEMO', False):
293
- return 'demo'
300
+ if os.environ.get("DEMO", False):
301
+ return "demo"
294
302
 
295
303
  if not self.state_loader:
296
- return 'none'
304
+ return "none"
297
305
 
298
306
  if self.state_loader.cloud_mode:
299
- return 'cloud'
307
+ return "cloud"
300
308
  elif self.state_loader.state_file:
301
- return 'file'
309
+ return "file"
302
310
  else:
303
- return 'none'
311
+ return "none"
304
312
 
305
313
 
306
314
  recce_context: Optional[RecceContext] = None