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/run.py CHANGED
@@ -2,14 +2,18 @@ import os
2
2
  import sys
3
3
  import time
4
4
  from datetime import datetime, timezone
5
- from typing import List
5
+ from typing import Dict, List, Tuple
6
6
 
7
7
  from deepdiff import DeepDiff
8
8
  from rich import box
9
9
  from rich.console import Console
10
10
  from rich.table import Table
11
11
 
12
- from recce.apis.check_func import create_check_from_run, create_check_without_run, purge_preset_checks
12
+ from recce.apis.check_func import (
13
+ create_check_from_run,
14
+ create_check_without_run,
15
+ purge_preset_checks,
16
+ )
13
17
  from recce.apis.run_func import submit_run
14
18
  from recce.config import RecceConfig
15
19
  from recce.core import default_context
@@ -19,13 +23,13 @@ from recce.summary import generate_markdown_summary
19
23
 
20
24
  def check_github_ci_env(**kwargs):
21
25
  """Check if the environment is GitHub CI"""
22
- if 'GITHUB_ACTIONS' not in os.environ:
26
+ if "GITHUB_ACTIONS" not in os.environ:
23
27
  return False, None
24
28
 
25
29
  # Get the PR number
26
- github_server_url = os.environ.get('GITHUB_SERVER_URL', 'https://github.com')
27
- github_repository = os.environ.get('GITHUB_REPOSITORY', '')
28
- github_ref_name = os.environ.get('GITHUB_REF_NAME', '')
30
+ github_server_url = os.environ.get("GITHUB_SERVER_URL", "https://github.com")
31
+ github_repository = os.environ.get("GITHUB_REPOSITORY", "")
32
+ github_ref_name = os.environ.get("GITHUB_REF_NAME", "")
29
33
  github_pull_request_url = f"{github_server_url}/{github_repository}/pull/{github_ref_name}"
30
34
 
31
35
  return True, github_pull_request_url
@@ -33,21 +37,22 @@ def check_github_ci_env(**kwargs):
33
37
 
34
38
  def load_preset_checks(checks: list):
35
39
  console = Console()
36
- table = Table(title='Recce Preset Checks', box=box.HORIZONTALS, title_style='bold dark_orange3')
37
- table.add_column('Name')
38
- table.add_column('Type')
39
- table.add_column('Description')
40
+ table = Table(title="Recce Preset Checks", box=box.HORIZONTALS, title_style="bold dark_orange3")
41
+ table.add_column("Name")
42
+ table.add_column("Type")
43
+ table.add_column("Description")
40
44
  for check in checks:
41
45
  is_check = False
42
- name = check.get('name')
43
- description = check.get('description', '')
44
- check_type = check.get('type')
45
- check_params = check.get('params', {})
46
- check_options = check.get('view_options', {})
47
-
48
- create_check_without_run(name, description, check_type, check_params, check_options, is_preset=True,
49
- is_checked=is_check)
50
- table.add_row(name, check_type.replace('_', ' ').title(), description.strip())
46
+ name = check.get("name")
47
+ description = check.get("description", "")
48
+ check_type = check.get("type")
49
+ check_params = check.get("params", {})
50
+ check_options = check.get("view_options", {})
51
+
52
+ create_check_without_run(
53
+ name, description, check_type, check_params, check_options, is_preset=True, is_checked=is_check
54
+ )
55
+ table.add_row(name, check_type.replace("_", " ").title(), description.strip())
51
56
  console.print(table)
52
57
 
53
58
 
@@ -55,28 +60,28 @@ def schema_diff_should_be_approved(check_params: dict) -> bool:
55
60
  try:
56
61
  context = default_context()
57
62
 
58
- if 'node_id' in check_params:
63
+ if "node_id" in check_params:
59
64
  # If the node_id is provided, then use it
60
- if isinstance(check_params['node_id'], str):
61
- selected_node_ids = [check_params['node_id']]
65
+ if isinstance(check_params["node_id"], str):
66
+ selected_node_ids = [check_params["node_id"]]
62
67
  else:
63
- selected_node_ids = check_params.get('node_id', [])
68
+ selected_node_ids = check_params.get("node_id", [])
64
69
  else:
65
70
  # Otherwise, select the nodes based on the select/exclude/packages/view_mode
66
71
  selected_node_ids = context.adapter.select_nodes(
67
- select=check_params.get('select'),
68
- exclude=check_params.get('exclude'),
69
- packages=check_params.get('packages'),
70
- view_mode=check_params.get('view_mode'),
72
+ select=check_params.get("select"),
73
+ exclude=check_params.get("exclude"),
74
+ packages=check_params.get("packages"),
75
+ view_mode=check_params.get("view_mode"),
71
76
  )
72
77
 
73
- selected_node_ids = [node for node in selected_node_ids if not node.startswith('test.')]
78
+ selected_node_ids = [node for node in selected_node_ids if not node.startswith("test.")]
74
79
 
75
80
  def _get_selected_node_columns_from_lineage(lineage, node_ids: list[str]):
76
81
  nodes = {}
77
- for node_id, node in lineage.get('nodes', {}).items():
82
+ for node_id, node in lineage.get("nodes", {}).items():
78
83
  if node_id in node_ids:
79
- nodes[node_id] = node.get('columns', {})
84
+ nodes[node_id] = node.get("columns", {})
80
85
  return nodes
81
86
 
82
87
  base_nodes = _get_selected_node_columns_from_lineage(context.get_lineage(base=True), selected_node_ids)
@@ -100,25 +105,25 @@ def run_should_be_approved(run):
100
105
  return False
101
106
  # If the row count are exactly the same, then the check should be approved
102
107
  for column, row_count_result in run.result.items():
103
- if row_count_result['base'] != row_count_result['curr']:
108
+ if row_count_result["base"] != row_count_result["curr"]:
104
109
  return False
105
110
  return True
106
111
  return False
107
112
 
108
113
 
109
- async def execute_preset_checks(preset_checks: list) -> (int, List[dict]):
114
+ async def execute_preset_checks(preset_checks: List, is_skip_query: bool) -> Tuple[int, List[Dict]]:
110
115
  """
111
116
  Execute the preset checks
112
117
  """
113
118
  console = Console()
114
119
  rc = 0
115
120
  failed_checks = []
116
- table = Table(title='Recce Preset Checks', box=box.HORIZONTALS, title_style='bold dark_orange3')
117
- table.add_column('Status')
118
- table.add_column('Name')
119
- table.add_column('Type')
120
- table.add_column('Execution Time')
121
- table.add_column('Failed Reason')
121
+ table = Table(title="Recce Preset Checks", box=box.HORIZONTALS, title_style="bold dark_orange3")
122
+ table.add_column("Status")
123
+ table.add_column("Name")
124
+ table.add_column("Type")
125
+ table.add_column("Execution Time")
126
+ table.add_column("Failed Reason")
122
127
 
123
128
  # Purge the existing preset checks before running the new ones
124
129
  purge_preset_checks()
@@ -126,11 +131,11 @@ async def execute_preset_checks(preset_checks: list) -> (int, List[dict]):
126
131
  # Execute the preset checks
127
132
  for check in preset_checks:
128
133
  run = None
129
- check_name = check.get('name')
130
- check_type = check.get('type')
131
- check_description = check.get('description', '')
132
- check_params = check.get('params') if check.get('params') else {}
133
- check_options = check.get('view_options', {})
134
+ check_name = check.get("name")
135
+ check_type = check.get("type")
136
+ check_description = check.get("description", "")
137
+ check_params = check.get("params") if check.get("params") else {}
138
+ check_options = check.get("view_options", {})
134
139
 
135
140
  try:
136
141
  # verify the check
@@ -138,59 +143,82 @@ async def execute_preset_checks(preset_checks: list) -> (int, List[dict]):
138
143
  raise ValueError(f"Invalid check type: {check_type}")
139
144
 
140
145
  start = time.time()
141
- if check_type in ['schema_diff', 'lineage_diff']:
142
- is_check = schema_diff_should_be_approved(check_params) if check_type == 'schema_diff' else False
143
- create_check_without_run(check_name, check_description, check_type, check_params, check_options,
144
- is_preset=True, is_checked=is_check)
146
+ if check_type in ["schema_diff", "lineage_diff"]:
147
+ is_check = schema_diff_should_be_approved(check_params) if check_type == "schema_diff" else False
148
+ create_check_without_run(
149
+ check_name,
150
+ check_description,
151
+ check_type,
152
+ check_params,
153
+ check_options,
154
+ is_preset=True,
155
+ is_checked=is_check,
156
+ )
145
157
  else:
146
- run, future = submit_run(check_type, params=check_params)
147
- await future
148
- is_check = run_should_be_approved(run)
149
- create_check_from_run(run.run_id, check_name, check_description, check_options, is_preset=True,
150
- is_checked=is_check)
158
+ if not is_skip_query:
159
+ run, future = submit_run(check_type, params=check_params)
160
+ await future
161
+ is_check = run_should_be_approved(run)
162
+ create_check_from_run(
163
+ run.run_id, check_name, check_description, check_options, is_preset=True, is_checked=is_check
164
+ )
165
+ else:
166
+ create_check_without_run(
167
+ check_name, check_description, check_type, check_params, check_options, is_preset=True
168
+ )
169
+ continue
151
170
 
152
171
  end = time.time()
153
- table.add_row('[[green]Success[/green]]', check_name, check_type.replace('_', ' ').title(),
154
- f'{end - start:.2f} seconds', 'N/A')
172
+ table.add_row(
173
+ "[[green]Success[/green]]",
174
+ check_name,
175
+ check_type.replace("_", " ").title(),
176
+ f"{end - start:.2f} seconds",
177
+ "N/A",
178
+ )
155
179
  except Exception as e:
156
180
  rc = 1
157
181
  if run is None:
158
- table.add_row('[[red]Error[/red]]', check_name, check_type.replace('_', ' ').title(), 'N/A', str(e))
159
- failed_checks.append({
160
- 'check_name': check_name,
161
- 'check_type': check_type,
162
- 'check_description': check_description,
163
- 'failed_type': 'error',
164
- 'failed_reason': str(e)
165
- })
182
+ table.add_row("[[red]Error[/red]]", check_name, check_type.replace("_", " ").title(), "N/A", str(e))
183
+ failed_checks.append(
184
+ {
185
+ "check_name": check_name,
186
+ "check_type": check_type,
187
+ "check_description": check_description,
188
+ "failed_type": "error",
189
+ "failed_reason": str(e),
190
+ }
191
+ )
166
192
  else:
167
193
  create_check_from_run(run.run_id, check_name, check_description, check_options, is_preset=True)
168
- table.add_row('[[red]Failed[/red]]', check_name, check_type.replace('_', ' ').title(), 'N/A', run.error)
169
- failed_checks.append({
170
- 'check_name': check_name,
171
- 'check_type': check_type,
172
- 'check_description': 'N/A',
173
- 'failed_type': 'failed',
174
- 'failed_reason': run.error
175
- })
194
+ table.add_row("[[red]Failed[/red]]", check_name, check_type.replace("_", " ").title(), "N/A", run.error)
195
+ failed_checks.append(
196
+ {
197
+ "check_name": check_name,
198
+ "check_type": check_type,
199
+ "check_description": "N/A",
200
+ "failed_type": "failed",
201
+ "failed_reason": run.error,
202
+ }
203
+ )
176
204
 
177
205
  console.print(table)
178
206
  return rc, failed_checks
179
207
 
180
208
 
181
- async def execute_state_checks(checks: list) -> (int, List[dict]):
209
+ async def execute_state_checks(checks: List, is_skip_query: bool) -> Tuple[int, List[Dict]]:
182
210
  """
183
211
  Execute the checks from loaded state
184
212
  """
185
213
  console = Console()
186
214
  rc = 0
187
215
  failed_checks = []
188
- table = Table(title='Recce Checks', box=box.HORIZONTALS, title_style='bold dark_orange3')
189
- table.add_column('Status')
190
- table.add_column('Name')
191
- table.add_column('Type')
192
- table.add_column('Execution Time')
193
- table.add_column('Failed Reason')
216
+ table = Table(title="Recce Checks", box=box.HORIZONTALS, title_style="bold dark_orange3")
217
+ table.add_column("Status")
218
+ table.add_column("Name")
219
+ table.add_column("Type")
220
+ table.add_column("Execution Time")
221
+ table.add_column("Failed Reason")
194
222
 
195
223
  # Execute loaded checks
196
224
  for check in checks:
@@ -210,33 +238,42 @@ async def execute_state_checks(checks: list) -> (int, List[dict]):
210
238
  raise ValueError(f"Invalid check type: {check_type}")
211
239
 
212
240
  start = time.time()
213
- if check_type not in ['schema_diff']:
241
+ if check_type not in ["schema_diff", "lineage_diff"] and not is_skip_query:
214
242
  run, future = submit_run(check_type, params=check_params, check_id=check_id)
215
243
  await future
216
244
 
217
245
  end = time.time()
218
- table.add_row('[[green]Success[/green]]', check_name, check_type.replace('_', ' ').title(),
219
- f'{end - start:.2f} seconds', 'N/A')
246
+ table.add_row(
247
+ "[[green]Success[/green]]",
248
+ check_name,
249
+ check_type.replace("_", " ").title(),
250
+ f"{end - start:.2f} seconds",
251
+ "N/A",
252
+ )
220
253
  except Exception as e:
221
254
  rc = 1
222
255
  if run is None:
223
- table.add_row('[[red]Error[/red]]', check_name, check_type.replace('_', ' ').title(), 'N/A', str(e))
224
- failed_checks.append({
225
- 'check_name': check_name,
226
- 'check_type': check_type,
227
- 'check_description': check_description,
228
- 'failed_type': 'error',
229
- 'failed_reason': str(e)
230
- })
256
+ table.add_row("[[red]Error[/red]]", check_name, check_type.replace("_", " ").title(), "N/A", str(e))
257
+ failed_checks.append(
258
+ {
259
+ "check_name": check_name,
260
+ "check_type": check_type,
261
+ "check_description": check_description,
262
+ "failed_type": "error",
263
+ "failed_reason": str(e),
264
+ }
265
+ )
231
266
  else:
232
- table.add_row('[[red]Failed[/red]]', check_name, check_type.replace('_', ' ').title(), 'N/A', run.error)
233
- failed_checks.append({
234
- 'check_name': check_name,
235
- 'check_type': check_type,
236
- 'check_description': 'N/A',
237
- 'failed_type': 'failed',
238
- 'failed_reason': run.error
239
- })
267
+ table.add_row("[[red]Failed[/red]]", check_name, check_type.replace("_", " ").title(), "N/A", run.error)
268
+ failed_checks.append(
269
+ {
270
+ "check_name": check_name,
271
+ "check_type": check_type,
272
+ "check_description": "N/A",
273
+ "failed_type": "failed",
274
+ "failed_reason": run.error,
275
+ }
276
+ )
240
277
 
241
278
  console.print(table)
242
279
  return rc, failed_checks
@@ -244,24 +281,27 @@ async def execute_state_checks(checks: list) -> (int, List[dict]):
244
281
 
245
282
  def process_failed_checks(failed_checks: List[dict], error_log=None):
246
283
  from py_markdown_table.markdown_table import markdown_table
284
+
247
285
  failed_check_table = []
248
286
  for check in failed_checks:
249
- name = check.get('check_name')
250
- check_type = check.get('check_type')
251
- failed_type = check.get('failed_type')
252
- failed_reason = check.get('failed_reason')
253
- failed_check_table.append({
254
- 'Name': name,
255
- 'Type': check_type,
256
- 'Kind of Failed': failed_type,
257
- 'Failed Reason': failed_reason.replace('\n', ' ')
258
- })
259
-
260
- content = '# Recce Runc Failed Checks\n'
261
- content += markdown_table(failed_check_table).set_params(quote=False, row_sep='markdown').get_markdown()
287
+ name = check.get("check_name")
288
+ check_type = check.get("check_type")
289
+ failed_type = check.get("failed_type")
290
+ failed_reason = check.get("failed_reason")
291
+ failed_check_table.append(
292
+ {
293
+ "Name": name,
294
+ "Type": check_type,
295
+ "Kind of Failed": failed_type,
296
+ "Failed Reason": failed_reason.replace("\n", " "),
297
+ }
298
+ )
299
+
300
+ content = "# Recce Runc Failed Checks\n"
301
+ content += markdown_table(failed_check_table).set_params(quote=False, row_sep="markdown").get_markdown()
262
302
 
263
303
  if error_log:
264
- with open(error_log, 'w') as f:
304
+ with open(error_log, "w", encoding="utf-8") as f:
265
305
  f.write(content)
266
306
  print(f"The failed checks are stored at '{error_log}'")
267
307
  else:
@@ -271,49 +311,53 @@ def process_failed_checks(failed_checks: List[dict], error_log=None):
271
311
  async def cli_run(output_state_file: str, **kwargs):
272
312
  """The main function of 'recce run' command. It will execute the default runs and store the state."""
273
313
  console = Console()
274
- error_log = kwargs.get('error_log')
275
- if kwargs.get('sqlmesh', False):
314
+ error_log = kwargs.get("error_log")
315
+ if kwargs.get("sqlmesh", False):
276
316
  console.print("[[red]Error[/red]] SQLMesh adapter is not supported.")
277
317
  sys.exit(1)
278
318
 
279
319
  from recce.core import load_context
320
+
280
321
  ctx = load_context(**kwargs)
281
322
 
282
- is_skip_query = kwargs.get('skip_query', False)
323
+ is_skip_query = kwargs.get("skip_query", False)
324
+ is_skip_check = kwargs.get("skip_check", False)
283
325
 
284
326
  # Prepare the artifact by collecting the lineage
285
327
  console.rule("DBT Artifacts")
286
328
  from recce.adapter.dbt_adapter import DbtAdapter
329
+
287
330
  dbt_adaptor: DbtAdapter = ctx.adapter
288
331
  dbt_adaptor.print_lineage_info()
289
332
 
290
333
  # Execute the preset checks
291
334
  rc = 0
292
335
  if ctx.state_loader.state is None:
293
- preset_checks = RecceConfig().get('checks')
294
- if is_skip_query or preset_checks is None or len(preset_checks) == 0:
336
+ preset_checks = RecceConfig().get("checks")
337
+ if is_skip_check or preset_checks is None or len(preset_checks) == 0:
295
338
  # Skip the preset checks
296
339
  pass
297
340
  else:
298
341
  console.rule("Preset checks")
299
- _, failed_checks = await execute_preset_checks(preset_checks)
342
+ _, failed_checks = await execute_preset_checks(preset_checks, is_skip_query)
300
343
  if failed_checks:
301
344
  console.print("[[yellow]Warning[/yellow]] Preset checks failed. Please see the failed reason.")
302
345
  process_failed_checks(failed_checks, error_log)
303
346
  else:
304
347
  state_checks = ctx.state_loader.state.checks
305
- if is_skip_query or state_checks is None or len(state_checks) == 0:
348
+ if is_skip_check or state_checks is None or len(state_checks) == 0:
306
349
  # Skip the checks in the state
307
350
  pass
308
351
  else:
309
352
  console.rule("Checks")
310
- _, failed_checks = await execute_state_checks(state_checks)
353
+ _, failed_checks = await execute_state_checks(state_checks, is_skip_query)
311
354
  if failed_checks:
312
355
  console.print("[[yellow]Warning[/yellow]] Checks failed. Please see the failed reason.")
313
356
  process_failed_checks(failed_checks, error_log)
314
357
 
315
358
  from recce.event import log_load_state
316
- log_load_state(command='run')
359
+
360
+ log_load_state(command="run")
317
361
 
318
362
  # Export the state
319
363
  console.rule("Export state")
@@ -321,12 +365,12 @@ async def cli_run(output_state_file: str, **kwargs):
321
365
  msg = ctx.state_loader.export(ctx.export_state())
322
366
  console.print(msg)
323
367
 
324
- summary_path = kwargs.get('summary')
368
+ summary_path = kwargs.get("summary")
325
369
  if summary_path:
326
370
  dirs = os.path.dirname(summary_path)
327
371
  if dirs:
328
372
  os.makedirs(dirs, exist_ok=True)
329
- with open(summary_path, 'w') as f:
373
+ with open(summary_path, "w", encoding="utf-8") as f:
330
374
  f.write(generate_markdown_summary(ctx))
331
375
  console.print(f"The summary is stored at '{summary_path}'")
332
376