recce-nightly 0.62.0.20250417__py3-none-any.whl → 1.30.0.20251221__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 (245) 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 +845 -461
  5. recce/adapter/dbt_adapter/dbt_version.py +3 -0
  6. recce/adapter/sqlmesh_adapter.py +24 -35
  7. recce/apis/check_api.py +59 -42
  8. recce/apis/check_events_api.py +353 -0
  9. recce/apis/check_func.py +41 -35
  10. recce/apis/run_api.py +25 -19
  11. recce/apis/run_func.py +64 -25
  12. recce/artifact.py +119 -51
  13. recce/cli.py +1301 -324
  14. recce/config.py +43 -34
  15. recce/connect_to_cloud.py +138 -0
  16. recce/core.py +55 -47
  17. recce/data/404/index.html +2 -0
  18. recce/data/404.html +2 -1
  19. recce/data/__next.@lineage.!KHNsb3Qp.__PAGE__.txt +7 -0
  20. recce/data/__next.@lineage.!KHNsb3Qp.txt +4 -0
  21. recce/data/__next.__PAGE__.txt +6 -0
  22. recce/data/__next._full.txt +32 -0
  23. recce/data/__next._head.txt +8 -0
  24. recce/data/__next._index.txt +14 -0
  25. recce/data/__next._tree.txt +8 -0
  26. recce/data/_next/static/chunks/025a7e3e3f9f40ae.js +1 -0
  27. recce/data/_next/static/chunks/0ce56d67ef5779ca.js +4 -0
  28. recce/data/_next/static/chunks/1a6a78780155dac7.js +48 -0
  29. recce/data/_next/static/chunks/1de8485918b9182a.css +2 -0
  30. recce/data/_next/static/chunks/1e4b1b50d1e34993.js +1 -0
  31. recce/data/_next/static/chunks/206d5d181e4c738e.js +1 -0
  32. recce/data/_next/static/chunks/2c357efc34c5b859.js +25 -0
  33. recce/data/_next/static/chunks/2e9d95d2d48c479c.js +1 -0
  34. recce/data/_next/static/chunks/2f016dc4a3edad2e.js +2 -0
  35. recce/data/_next/static/chunks/313251962d698f7c.js +1 -0
  36. recce/data/_next/static/chunks/3a9f021f38eb5574.css +1 -0
  37. recce/data/_next/static/chunks/40079da8d2b8f651.js +1 -0
  38. recce/data/_next/static/chunks/4599182bffb64661.js +38 -0
  39. recce/data/_next/static/chunks/4e62f6e184173580.js +1 -0
  40. recce/data/_next/static/chunks/5c4dfb0d09eaa401.js +1 -0
  41. recce/data/_next/static/chunks/69e4f06ccfdfc3ac.js +1 -0
  42. recce/data/_next/static/chunks/6b206cb4707d6bee.js +1 -0
  43. recce/data/_next/static/chunks/6d8557f062aa4386.css +1 -0
  44. recce/data/_next/static/chunks/7fbe3650bd83b6b5.js +1 -0
  45. recce/data/_next/static/chunks/83fa823a825674f6.js +1 -0
  46. recce/data/_next/static/chunks/848a6c9b5f55f7ed.js +1 -0
  47. recce/data/_next/static/chunks/859462b0858aef88.css +2 -0
  48. recce/data/_next/static/chunks/923964f18c87d0f1.css +1 -0
  49. recce/data/_next/static/chunks/939390f911895d7c.js +48 -0
  50. recce/data/_next/static/chunks/99a9817237a07f43.js +1 -0
  51. recce/data/_next/static/chunks/9fed8b4b2b924054.js +5 -0
  52. recce/data/_next/static/chunks/b6949f6c5892110c.js +1 -0
  53. recce/data/_next/static/chunks/b851a1d3f8149828.js +1 -0
  54. recce/data/_next/static/chunks/c734f9ad957de0b4.js +1 -0
  55. recce/data/_next/static/chunks/cdde321b0ec75717.js +2 -0
  56. recce/data/_next/static/chunks/d0f91117d77ff844.css +1 -0
  57. recce/data/_next/static/chunks/d6c8667911c2500f.js +1 -0
  58. recce/data/_next/static/chunks/da8dab68c02752cf.js +74 -0
  59. recce/data/_next/static/chunks/dc074049c9d12d97.js +109 -0
  60. recce/data/_next/static/chunks/ee7f1a8227342421.js +1 -0
  61. recce/data/_next/static/chunks/fa2f4e56c2fccc73.js +1 -0
  62. recce/data/_next/static/chunks/turbopack-1fad664f62979b93.js +3 -0
  63. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  64. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  65. recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
  66. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  67. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
  68. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  69. recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
  70. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  71. recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
  72. recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
  73. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  74. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_buildManifest.js +11 -0
  75. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_clientMiddlewareManifest.json +1 -0
  76. recce/data/_not-found/__next._full.txt +24 -0
  77. recce/data/_not-found/__next._head.txt +8 -0
  78. recce/data/_not-found/__next._index.txt +13 -0
  79. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  80. recce/data/_not-found/__next._not-found.txt +4 -0
  81. recce/data/_not-found/__next._tree.txt +6 -0
  82. recce/data/_not-found/index.html +2 -0
  83. recce/data/_not-found/index.txt +24 -0
  84. recce/data/auth_callback.html +68 -0
  85. recce/data/checks/__next.@lineage.__DEFAULT__.txt +7 -0
  86. recce/data/checks/__next._full.txt +39 -0
  87. recce/data/checks/__next._head.txt +8 -0
  88. recce/data/checks/__next._index.txt +14 -0
  89. recce/data/checks/__next._tree.txt +8 -0
  90. recce/data/checks/__next.checks.__PAGE__.txt +10 -0
  91. recce/data/checks/__next.checks.txt +4 -0
  92. recce/data/checks/index.html +2 -0
  93. recce/data/checks/index.txt +39 -0
  94. recce/data/imgs/reload-image.svg +4 -0
  95. recce/data/index.html +2 -27
  96. recce/data/index.txt +32 -7
  97. recce/data/lineage/__next.@lineage.__DEFAULT__.txt +7 -0
  98. recce/data/lineage/__next._full.txt +39 -0
  99. recce/data/lineage/__next._head.txt +8 -0
  100. recce/data/lineage/__next._index.txt +14 -0
  101. recce/data/lineage/__next._tree.txt +8 -0
  102. recce/data/lineage/__next.lineage.__PAGE__.txt +10 -0
  103. recce/data/lineage/__next.lineage.txt +4 -0
  104. recce/data/lineage/index.html +2 -0
  105. recce/data/lineage/index.txt +39 -0
  106. recce/data/query/__next.@lineage.__DEFAULT__.txt +7 -0
  107. recce/data/query/__next._full.txt +37 -0
  108. recce/data/query/__next._head.txt +8 -0
  109. recce/data/query/__next._index.txt +14 -0
  110. recce/data/query/__next._tree.txt +8 -0
  111. recce/data/query/__next.query.__PAGE__.txt +9 -0
  112. recce/data/query/__next.query.txt +4 -0
  113. recce/data/query/index.html +2 -0
  114. recce/data/query/index.txt +37 -0
  115. recce/diff.py +6 -12
  116. recce/event/CONFIG.bak +1 -0
  117. recce/event/__init__.py +86 -74
  118. recce/event/collector.py +33 -22
  119. recce/event/track.py +49 -27
  120. recce/exceptions.py +1 -1
  121. recce/git.py +7 -7
  122. recce/github.py +57 -53
  123. recce/mcp_server.py +725 -0
  124. recce/models/__init__.py +4 -1
  125. recce/models/check.py +438 -21
  126. recce/models/run.py +1 -0
  127. recce/models/types.py +134 -28
  128. recce/pull_request.py +27 -25
  129. recce/run.py +179 -122
  130. recce/server.py +394 -104
  131. recce/state/__init__.py +31 -0
  132. recce/state/cloud.py +644 -0
  133. recce/state/const.py +26 -0
  134. recce/state/local.py +56 -0
  135. recce/state/state.py +119 -0
  136. recce/state/state_loader.py +174 -0
  137. recce/summary.py +196 -149
  138. recce/tasks/__init__.py +19 -3
  139. recce/tasks/core.py +11 -13
  140. recce/tasks/dataframe.py +82 -18
  141. recce/tasks/histogram.py +69 -34
  142. recce/tasks/lineage.py +2 -2
  143. recce/tasks/profile.py +152 -86
  144. recce/tasks/query.py +180 -89
  145. recce/tasks/rowcount.py +37 -31
  146. recce/tasks/schema.py +18 -15
  147. recce/tasks/top_k.py +35 -35
  148. recce/tasks/utils.py +147 -0
  149. recce/tasks/valuediff.py +247 -155
  150. recce/util/__init__.py +3 -0
  151. recce/util/api_token.py +80 -0
  152. recce/util/breaking.py +105 -100
  153. recce/util/cll.py +274 -219
  154. recce/util/cloud/__init__.py +15 -0
  155. recce/util/cloud/base.py +115 -0
  156. recce/util/cloud/check_events.py +190 -0
  157. recce/util/cloud/checks.py +242 -0
  158. recce/util/io.py +22 -17
  159. recce/util/lineage.py +65 -16
  160. recce/util/logger.py +1 -1
  161. recce/util/onboarding_state.py +45 -0
  162. recce/util/perf_tracking.py +85 -0
  163. recce/util/recce_cloud.py +347 -72
  164. recce/util/singleton.py +4 -4
  165. recce/util/startup_perf.py +121 -0
  166. recce/yaml/__init__.py +7 -10
  167. recce_nightly-1.30.0.20251221.dist-info/METADATA +195 -0
  168. recce_nightly-1.30.0.20251221.dist-info/RECORD +183 -0
  169. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/WHEEL +1 -2
  170. recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
  171. recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
  172. recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
  173. recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
  174. recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
  175. recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
  176. recce/data/_next/static/chunks/500-e51c92a025a51234.js +0 -65
  177. recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
  178. recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
  179. recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
  180. recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
  181. recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
  182. recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
  183. recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
  184. recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
  185. recce/data/_next/static/chunks/app/page-9adc25782272ed2e.js +0 -1
  186. recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
  187. recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
  188. recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
  189. recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
  190. recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
  191. recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
  192. recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
  193. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  194. recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
  195. recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
  196. recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
  197. recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
  198. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  199. recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
  200. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  201. recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
  202. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
  203. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  204. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  205. recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
  206. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  207. recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
  208. recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
  209. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  210. recce/data/_next/static/qiyFlux77VkhxiceAJe_F/_buildManifest.js +0 -1
  211. recce/state.py +0 -753
  212. recce_nightly-0.62.0.20250417.dist-info/METADATA +0 -311
  213. recce_nightly-0.62.0.20250417.dist-info/RECORD +0 -139
  214. recce_nightly-0.62.0.20250417.dist-info/top_level.txt +0 -2
  215. tests/__init__.py +0 -0
  216. tests/adapter/__init__.py +0 -0
  217. tests/adapter/dbt_adapter/__init__.py +0 -0
  218. tests/adapter/dbt_adapter/conftest.py +0 -13
  219. tests/adapter/dbt_adapter/dbt_test_helper.py +0 -283
  220. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -40
  221. tests/adapter/dbt_adapter/test_dbt_cll.py +0 -102
  222. tests/adapter/dbt_adapter/test_selector.py +0 -177
  223. tests/tasks/__init__.py +0 -0
  224. tests/tasks/conftest.py +0 -4
  225. tests/tasks/test_histogram.py +0 -137
  226. tests/tasks/test_lineage.py +0 -42
  227. tests/tasks/test_preset_checks.py +0 -50
  228. tests/tasks/test_profile.py +0 -73
  229. tests/tasks/test_query.py +0 -151
  230. tests/tasks/test_row_count.py +0 -116
  231. tests/tasks/test_schema.py +0 -99
  232. tests/tasks/test_top_k.py +0 -73
  233. tests/tasks/test_valuediff.py +0 -74
  234. tests/test_cli.py +0 -122
  235. tests/test_config.py +0 -45
  236. tests/test_core.py +0 -27
  237. tests/test_dbt.py +0 -36
  238. tests/test_pull_request.py +0 -130
  239. tests/test_server.py +0 -98
  240. tests/test_state.py +0 -123
  241. tests/test_summary.py +0 -57
  242. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  243. /recce/data/_next/static/{qiyFlux77VkhxiceAJe_F → nX-Uz0AH6Tc6hIQUFGqaB}/_ssgManifest.js +0 -0
  244. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/entry_points.txt +0 -0
  245. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/licenses/LICENSE +0 -0
recce/apis/check_func.py CHANGED
@@ -5,13 +5,13 @@ from fastapi import HTTPException
5
5
 
6
6
  from recce.apis.run_func import generate_run_name
7
7
  from recce.core import default_context
8
- from recce.models import RunDAO, RunType, Check, CheckDAO
8
+ from recce.models import Check, CheckDAO, RunDAO, RunType
9
9
 
10
10
 
11
11
  def validate_schema_diff_check(params):
12
- node_id = params.get('node_id')
12
+ node_id = params.get("node_id")
13
13
  if node_id is None:
14
- raise HTTPException(status_code=400, detail='node_id is required for schema diff')
14
+ raise HTTPException(status_code=400, detail="node_id is required for schema diff")
15
15
  node_name = default_context().get_node_name_by_id(node_id)
16
16
  if node_name is None:
17
17
  raise HTTPException(status_code=400, detail=f"node_id '{node_id}' not found in dbt manifest")
@@ -45,8 +45,8 @@ def _get_ref_model(sql_template: str) -> Optional[str]:
45
45
  def _generate_check_name(check_type, params, view_options):
46
46
  now = datetime.utcnow().strftime("%d %b %Y")
47
47
  if check_type == RunType.SCHEMA_DIFF:
48
- if params.get('node_id'):
49
- nodeIds = params.get('node_id') if isinstance(params.get('node_id'), list) else [params.get('node_id')]
48
+ if params.get("node_id"):
49
+ nodeIds = params.get("node_id") if isinstance(params.get("node_id"), list) else [params.get("node_id")]
50
50
  if len(nodeIds) == 1:
51
51
  node_name = get_node_name_by_id(nodeIds[0])
52
52
  return f"schema diff of {node_name}".capitalize()
@@ -54,7 +54,7 @@ def _generate_check_name(check_type, params, view_options):
54
54
  return f"schema diff of {len(nodeIds)} nodes".capitalize()
55
55
  return f"{'schema diff'.capitalize()} - {now}"
56
56
  elif check_type == RunType.LINEAGE_DIFF:
57
- nodes = view_options.get('node_ids') if view_options else params.get('node_ids')
57
+ nodes = view_options.get("node_ids") if view_options else params.get("node_ids")
58
58
  if nodes is not None:
59
59
  return f"lineage diff of {len(nodes)} nodes".capitalize()
60
60
  return f"{'lineage diff'.capitalize()} - {now}"
@@ -62,10 +62,11 @@ def _generate_check_name(check_type, params, view_options):
62
62
  return f"{'check'.capitalize()} - {now}"
63
63
 
64
64
 
65
- def create_check_from_run(run_id, check_name=None, check_description='', check_view_options=None, is_preset=False,
66
- is_checked=False):
65
+ def create_check_from_run(
66
+ run_id, check_name=None, check_description="", check_view_options=None, is_preset=False, is_checked=False
67
+ ):
67
68
  if run_id is None:
68
- raise ValueError('run_id is required')
69
+ raise ValueError("run_id is required")
69
70
 
70
71
  run = RunDAO().find_run_by_id(run_id)
71
72
  if run is None:
@@ -76,31 +77,36 @@ def create_check_from_run(run_id, check_name=None, check_description='', check_v
76
77
 
77
78
  _validate_check(run_type, run_params)
78
79
  name = check_name if check_name is not None else generate_run_name(run)
79
- check = Check(name=name,
80
- description=check_description,
81
- type=run_type,
82
- params=run_params,
83
- view_options=check_view_options,
84
- is_preset=is_preset,
85
- is_checked=is_checked)
86
- CheckDAO().create(check)
87
- run.check_id = check.check_id
88
-
89
- return check
90
-
91
-
92
- def create_check_without_run(check_name, check_description, check_type, params, check_view_options, is_preset=False,
93
- is_checked=False):
80
+ check = Check(
81
+ name=name,
82
+ description=check_description,
83
+ type=run_type,
84
+ params=run_params,
85
+ view_options=check_view_options,
86
+ is_preset=is_preset,
87
+ is_checked=is_checked,
88
+ )
89
+ new_check = CheckDAO().create(check)
90
+ run.check_id = new_check.check_id
91
+
92
+ return new_check
93
+
94
+
95
+ def create_check_without_run(
96
+ check_name, check_description, check_type, params, check_view_options, is_preset=False, is_checked=False
97
+ ):
94
98
  name = check_name if check_name is not None else _generate_check_name(check_type, params, check_view_options)
95
- check = Check(name=name,
96
- description=check_description,
97
- type=check_type,
98
- params=params,
99
- view_options=check_view_options,
100
- is_preset=is_preset,
101
- is_checked=is_checked)
102
- CheckDAO().create(check)
103
- return check
99
+ check = Check(
100
+ name=name,
101
+ description=check_description,
102
+ type=check_type,
103
+ params=params,
104
+ view_options=check_view_options,
105
+ is_preset=is_preset,
106
+ is_checked=is_checked,
107
+ )
108
+ new_check = CheckDAO().create(check)
109
+ return new_check
104
110
 
105
111
 
106
112
  def purge_preset_checks():
@@ -119,6 +125,6 @@ def export_persistent_state():
119
125
  if state_loader is not None:
120
126
  is_conflict = state_loader.check_conflict()
121
127
  if is_conflict:
122
- ctx.sync_state('merge')
128
+ ctx.sync_state("merge")
123
129
  else:
124
- ctx.sync_state('overwrite')
130
+ ctx.sync_state("overwrite")
recce/apis/run_api.py CHANGED
@@ -1,16 +1,16 @@
1
1
  import asyncio
2
- from typing import Optional, List
2
+ from typing import List, Optional
3
3
  from uuid import UUID
4
4
 
5
5
  from fastapi import APIRouter, HTTPException, Query
6
6
  from pydantic import BaseModel
7
7
 
8
- from recce.apis.run_func import submit_run, cancel_run, materialize_run_results
8
+ from recce.apis.run_func import cancel_run, materialize_run_results, submit_run
9
9
  from recce.event import log_api_event
10
10
  from recce.exceptions import RecceException
11
11
  from recce.models import RunDAO
12
12
 
13
- run_router = APIRouter(tags=['run'])
13
+ run_router = APIRouter(tags=["run"])
14
14
 
15
15
 
16
16
  class CreateRunIn(BaseModel):
@@ -23,10 +23,13 @@ class CreateRunIn(BaseModel):
23
23
 
24
24
  @run_router.post("/runs", status_code=201)
25
25
  async def create_run_handler(input: CreateRunIn):
26
- log_api_event('create_run', dict(
27
- type=input.type,
28
- track_props=input.track_props,
29
- ))
26
+ log_api_event(
27
+ "create_run",
28
+ dict(
29
+ type=input.type,
30
+ track_props=input.track_props,
31
+ ),
32
+ )
30
33
  try:
31
34
  run, future = submit_run(input.type, input.params)
32
35
  except RecceException as e:
@@ -51,7 +54,7 @@ async def cancel_run_handler(run_id: UUID):
51
54
  async def wait_run_handler(run_id: UUID, timeout: int = Query(None, description="Maximum number of seconds to wait")):
52
55
  run = RunDAO().find_run_by_id(run_id)
53
56
  if run is None:
54
- raise HTTPException(status_code=404, detail='Not Found')
57
+ raise HTTPException(status_code=404, detail="Not Found")
55
58
 
56
59
  start_time = asyncio.get_event_loop().time()
57
60
  while run.result is None and run.error is None:
@@ -65,18 +68,21 @@ async def wait_run_handler(run_id: UUID, timeout: int = Query(None, description=
65
68
  async def list_run_handler():
66
69
  runs = RunDAO().list() or []
67
70
 
68
- result = [{
69
- 'run_id': run.run_id,
70
- 'run_at': run.run_at,
71
- 'name': run.name,
72
- 'type': run.type,
73
- 'params': run.params,
74
- 'status': run.status,
75
- 'check_id': run.check_id,
76
- } for run in runs]
71
+ result = [
72
+ {
73
+ "run_id": run.run_id,
74
+ "run_at": run.run_at,
75
+ "name": run.name,
76
+ "type": run.type,
77
+ "params": run.params,
78
+ "status": run.status,
79
+ "check_id": run.check_id,
80
+ }
81
+ for run in runs
82
+ ]
77
83
 
78
84
  # sort by run_at
79
- result = sorted(result, key=lambda x: x['run_at'], reverse=True)
85
+ result = sorted(result, key=lambda x: x["run_at"], reverse=True)
80
86
 
81
87
  return result
82
88
 
@@ -101,7 +107,7 @@ async def search_runs_handler(search: SearchRunsIn):
101
107
  result.append(run)
102
108
 
103
109
  if search.limit:
104
- return result[-search.limit:]
110
+ return result[-search.limit :]
105
111
 
106
112
  return result
107
113
 
recce/apis/run_func.py CHANGED
@@ -4,11 +4,11 @@ from typing import List, Optional
4
4
 
5
5
  from recce.core import default_context
6
6
  from recce.exceptions import RecceException
7
- from recce.models import RunType, Run, RunDAO
7
+ from recce.models import Run, RunDAO, RunType
8
8
  from recce.models.types import RunStatus
9
9
 
10
10
  running_tasks = {}
11
- logger = logging.getLogger('uvicorn')
11
+ logger = logging.getLogger("uvicorn")
12
12
 
13
13
 
14
14
  def _get_ref_model(sql_template: str) -> Optional[str]:
@@ -33,26 +33,26 @@ def generate_run_name(run):
33
33
  now = dateutil.parser.parse(run.run_at)
34
34
 
35
35
  if run_type == RunType.QUERY:
36
- ref = _get_ref_model(params.get('sql_template'))
36
+ ref = _get_ref_model(params.get("sql_template"))
37
37
  if ref:
38
38
  return f"query of {ref}".capitalize()
39
39
  return f"{'query'.capitalize()} - {now}"
40
40
  elif run_type == RunType.QUERY_DIFF:
41
- ref = _get_ref_model(params.get('sql_template'))
41
+ ref = _get_ref_model(params.get("sql_template"))
42
42
  if ref:
43
43
  return f"query diff of {ref}".capitalize()
44
44
  return f"{'query diff'.capitalize()} - {now}"
45
45
  elif run_type == RunType.VALUE_DIFF:
46
- model = params.get('model')
46
+ model = params.get("model")
47
47
  return f"value diff of {model}".capitalize()
48
48
  elif run_type == RunType.VALUE_DIFF_DETAIL:
49
- model = params.get('model')
49
+ model = params.get("model")
50
50
  return f"value diff detail of {model}".capitalize()
51
51
  elif run_type == RunType.PROFILE_DIFF:
52
- model = params.get('model')
52
+ model = params.get("model")
53
53
  return f"profile diff of {model}".capitalize()
54
54
  elif run_type == RunType.ROW_COUNT_DIFF:
55
- nodes = params.get('node_names')
55
+ nodes = params.get("node_names")
56
56
  if nodes:
57
57
  if len(nodes) == 1:
58
58
  node = nodes[0]
@@ -62,23 +62,27 @@ def generate_run_name(run):
62
62
  else:
63
63
  return "row count of multiple nodes".capitalize()
64
64
  elif run_type == RunType.TOP_K_DIFF:
65
- model = params.get('model')
66
- column = params.get('column_name')
65
+ model = params.get("model")
66
+ column = params.get("column_name")
67
67
  return f"top-k diff of {model}.{column} ".capitalize()
68
68
  elif run_type == RunType.HISTOGRAM_DIFF:
69
- model = params.get('model')
70
- column = params.get('column_name')
69
+ model = params.get("model")
70
+ column = params.get("column_name")
71
71
  return f"histogram diff of {model}.{column} ".capitalize()
72
72
  else:
73
73
  return f"{'run'.capitalize()} - {now}"
74
74
 
75
75
 
76
76
  def create_task(run_type: RunType, params: dict):
77
- if default_context().adapter_type == 'sqlmesh':
78
- from recce.adapter.sqlmesh_adapter import sqlmesh_supported_registry as sqlmesh_registry
77
+ if default_context().adapter_type == "sqlmesh":
78
+ from recce.adapter.sqlmesh_adapter import (
79
+ sqlmesh_supported_registry as sqlmesh_registry,
80
+ )
81
+
79
82
  registry = sqlmesh_registry
80
83
  else:
81
84
  from recce.adapter.dbt_adapter import dbt_supported_registry as dbt_registry
85
+
82
86
  registry = dbt_registry
83
87
 
84
88
  taskClz = registry.get(run_type)
@@ -101,6 +105,7 @@ def submit_run(type, params, check_id=None):
101
105
  context = default_context()
102
106
  if context.review_mode is True:
103
107
  from recce.adapter.dbt_adapter import DbtAdapter
108
+
104
109
  dbt_adaptor: DbtAdapter = context.adapter
105
110
  if dbt_adaptor.adapter is None:
106
111
  raise RecceException("Recce Server is not launched under DBT project folder.")
@@ -113,18 +118,23 @@ def submit_run(type, params, check_id=None):
113
118
  running_tasks[run.run_id] = task
114
119
 
115
120
  def progress_listener(message=None, percentage=None):
116
- run.progress = {'message': message, 'percentage': percentage}
121
+ run.progress = {"message": message, "percentage": percentage}
117
122
 
118
123
  task.progress_listener = progress_listener
119
124
 
120
- async def update_run_result(run_id, result, error):
125
+ async def update_run_result(run, result, error, updated_params=None):
126
+ """Update run with result, error, and optionally updated params."""
121
127
  if run is None:
122
128
  return
123
129
  if result is not None:
124
130
  run.result = result
125
131
  run.status = RunStatus.FINISHED
132
+ if updated_params is not None:
133
+ # Merge updated params (preserves any fields not in updated_params)
134
+ run.params.update(updated_params)
126
135
  if error is not None:
127
- run.error = str(error)
136
+ failed_reason = str(error) if str(error) != "None" else repr(error)
137
+ run.error = failed_reason
128
138
  if run.status != RunStatus.CANCELLED:
129
139
  run.status = RunStatus.FAILED
130
140
  run.progress = None
@@ -132,13 +142,42 @@ def submit_run(type, params, check_id=None):
132
142
  def fn():
133
143
  try:
134
144
  result = task.execute()
135
- asyncio.run_coroutine_threadsafe(update_run_result(run.run_id, result, None), loop)
145
+
146
+ # Extract updated params from task after execution
147
+ updated_params = None
148
+ if hasattr(task, "params") and task.params is not None:
149
+ # Serialization logic:
150
+ # - Most tasks use Pydantic models (v2: model_dump, v1: dict)
151
+ # - Some tasks may use plain dicts
152
+ # - If params is an unexpected type, log a warning for debugging
153
+ # - Handle the case where model_dump() or dict() raises an exception.
154
+ try:
155
+ if hasattr(task.params, "model_dump"):
156
+ updated_params = task.params.model_dump()
157
+ elif hasattr(task.params, "dict"):
158
+ updated_params = task.params.dict()
159
+ elif isinstance(task.params, dict):
160
+ updated_params = task.params
161
+ else:
162
+ logger.warning(
163
+ f"Could not serialize task.params for run_id={run.run_id}: "
164
+ f"unexpected type {type(task.params)} with value {repr(task.params)}"
165
+ )
166
+ except Exception as e:
167
+ logger.warning(f"Failed to serialize task.params: {e}")
168
+ updated_params = None
169
+
170
+ asyncio.run_coroutine_threadsafe(update_run_result(run, result, None, updated_params), loop)
136
171
  return result
137
172
  except BaseException as e:
138
- asyncio.run_coroutine_threadsafe(update_run_result(run.run_id, None, e), loop)
173
+ asyncio.run_coroutine_threadsafe(update_run_result(run, None, e, None), loop)
139
174
  if isinstance(e, RecceException) and e.is_raise is False:
140
175
  return None
141
- failed_reason = str(e).replace('. ', ".\n")
176
+ import sentry_sdk
177
+
178
+ sentry_sdk.capture_exception(e)
179
+ failed_reason = str(e) if str(e) != "None" else repr(e)
180
+ failed_reason = failed_reason.replace(". ", ".\n")
142
181
  logger.error(f"Failed to execute {run_type} task: {failed_reason}")
143
182
  return None
144
183
 
@@ -160,7 +199,7 @@ def cancel_run(run_id):
160
199
 
161
200
 
162
201
  def materialize_run_results(runs: List[Run], nodes: List[str] = None):
163
- '''
202
+ """
164
203
  Materialize the run results for nodes. It walks through all runs and get the last results for primary run types.
165
204
 
166
205
  The result format
@@ -176,11 +215,11 @@ def materialize_run_results(runs: List[Run], nodes: List[str] = None):
176
215
  },
177
216
  },
178
217
  }
179
- '''
218
+ """
180
219
 
181
220
  context = default_context()
182
221
  if context:
183
- mame_to_unique_id = context.build_name_to_unique_id_index(excluded_types={'semantic_model', 'metric'})
222
+ mame_to_unique_id = context.build_name_to_unique_id_index(excluded_types={"semantic_model", "metric"})
184
223
  else:
185
224
  mame_to_unique_id = {}
186
225
 
@@ -201,7 +240,7 @@ def materialize_run_results(runs: List[Run], nodes: List[str] = None):
201
240
  node_result = result[key] = {}
202
241
  else:
203
242
  node_result = result.get(key)
204
- node_result['row_count_diff'] = {'run_id': run.run_id, 'result': node_run_result}
243
+ node_result["row_count_diff"] = {"run_id": run.run_id, "result": node_run_result}
205
244
  elif run.type == RunType.ROW_COUNT:
206
245
  for model_name, node_run_result in run.result.items():
207
246
  key = mame_to_unique_id.get(model_name, model_name)
@@ -214,5 +253,5 @@ def materialize_run_results(runs: List[Run], nodes: List[str] = None):
214
253
  node_result = result[key] = {}
215
254
  else:
216
255
  node_result = result.get(key)
217
- node_result['row_count'] = {'run_id': run.run_id, 'result': node_run_result}
256
+ node_result["row_count"] = {"run_id": run.run_id, "result": node_run_result}
218
257
  return result