recce-nightly 1.10.0.20250625__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 (229) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +5 -0
  3. recce/adapter/dbt_adapter/__init__.py +343 -245
  4. recce/apis/check_api.py +20 -14
  5. recce/apis/check_events_api.py +353 -0
  6. recce/apis/check_func.py +5 -5
  7. recce/apis/run_func.py +32 -3
  8. recce/artifact.py +76 -3
  9. recce/cli.py +705 -82
  10. recce/config.py +2 -2
  11. recce/connect_to_cloud.py +1 -1
  12. recce/core.py +3 -3
  13. recce/data/404/index.html +2 -0
  14. recce/data/404.html +2 -22
  15. recce/data/__next.@lineage.!KHNsb3Qp.__PAGE__.txt +7 -0
  16. recce/data/__next.@lineage.!KHNsb3Qp.txt +4 -0
  17. recce/data/__next.__PAGE__.txt +6 -0
  18. recce/data/__next._full.txt +32 -0
  19. recce/data/__next._head.txt +8 -0
  20. recce/data/__next._index.txt +14 -0
  21. recce/data/__next._tree.txt +8 -0
  22. recce/data/_next/static/chunks/025a7e3e3f9f40ae.js +1 -0
  23. recce/data/_next/static/chunks/0ce56d67ef5779ca.js +4 -0
  24. recce/data/_next/static/chunks/1a6a78780155dac7.js +48 -0
  25. recce/data/_next/static/chunks/1de8485918b9182a.css +2 -0
  26. recce/data/_next/static/chunks/1e4b1b50d1e34993.js +1 -0
  27. recce/data/_next/static/chunks/206d5d181e4c738e.js +1 -0
  28. recce/data/_next/static/chunks/2c357efc34c5b859.js +25 -0
  29. recce/data/_next/static/chunks/2e9d95d2d48c479c.js +1 -0
  30. recce/data/_next/static/chunks/2f016dc4a3edad2e.js +2 -0
  31. recce/data/_next/static/chunks/313251962d698f7c.js +1 -0
  32. recce/data/_next/static/chunks/3a9f021f38eb5574.css +1 -0
  33. recce/data/_next/static/chunks/40079da8d2b8f651.js +1 -0
  34. recce/data/_next/static/chunks/4599182bffb64661.js +38 -0
  35. recce/data/_next/static/chunks/4e62f6e184173580.js +1 -0
  36. recce/data/_next/static/chunks/5c4dfb0d09eaa401.js +1 -0
  37. recce/data/_next/static/chunks/69e4f06ccfdfc3ac.js +1 -0
  38. recce/data/_next/static/chunks/6b206cb4707d6bee.js +1 -0
  39. recce/data/_next/static/chunks/6d8557f062aa4386.css +1 -0
  40. recce/data/_next/static/chunks/7fbe3650bd83b6b5.js +1 -0
  41. recce/data/_next/static/chunks/83fa823a825674f6.js +1 -0
  42. recce/data/_next/static/chunks/848a6c9b5f55f7ed.js +1 -0
  43. recce/data/_next/static/chunks/859462b0858aef88.css +2 -0
  44. recce/data/_next/static/chunks/923964f18c87d0f1.css +1 -0
  45. recce/data/_next/static/chunks/939390f911895d7c.js +48 -0
  46. recce/data/_next/static/chunks/99a9817237a07f43.js +1 -0
  47. recce/data/_next/static/chunks/9fed8b4b2b924054.js +5 -0
  48. recce/data/_next/static/chunks/b6949f6c5892110c.js +1 -0
  49. recce/data/_next/static/chunks/b851a1d3f8149828.js +1 -0
  50. recce/data/_next/static/chunks/c734f9ad957de0b4.js +1 -0
  51. recce/data/_next/static/chunks/cdde321b0ec75717.js +2 -0
  52. recce/data/_next/static/chunks/d0f91117d77ff844.css +1 -0
  53. recce/data/_next/static/chunks/d6c8667911c2500f.js +1 -0
  54. recce/data/_next/static/chunks/da8dab68c02752cf.js +74 -0
  55. recce/data/_next/static/chunks/dc074049c9d12d97.js +109 -0
  56. recce/data/_next/static/chunks/ee7f1a8227342421.js +1 -0
  57. recce/data/_next/static/chunks/fa2f4e56c2fccc73.js +1 -0
  58. recce/data/_next/static/chunks/turbopack-1fad664f62979b93.js +3 -0
  59. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  60. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  61. recce/data/_next/static/media/{montserrat-cyrillic-800-normal.bd5c9f50.woff → montserrat-cyrillic-800-normal.f9d58125.woff} +0 -0
  62. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  63. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  64. recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → montserrat-latin-800-normal.d5761935.woff} +0 -0
  65. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  66. recce/data/_next/static/media/{montserrat-latin-ext-800-normal.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
  67. recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
  68. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  69. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_buildManifest.js +11 -0
  70. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_clientMiddlewareManifest.json +1 -0
  71. recce/data/_not-found/__next._full.txt +24 -0
  72. recce/data/_not-found/__next._head.txt +8 -0
  73. recce/data/_not-found/__next._index.txt +13 -0
  74. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  75. recce/data/_not-found/__next._not-found.txt +4 -0
  76. recce/data/_not-found/__next._tree.txt +6 -0
  77. recce/data/_not-found/index.html +2 -0
  78. recce/data/_not-found/index.txt +24 -0
  79. recce/data/auth_callback.html +1 -1
  80. recce/data/checks/__next.@lineage.__DEFAULT__.txt +7 -0
  81. recce/data/checks/__next._full.txt +39 -0
  82. recce/data/checks/__next._head.txt +8 -0
  83. recce/data/checks/__next._index.txt +14 -0
  84. recce/data/checks/__next._tree.txt +8 -0
  85. recce/data/checks/__next.checks.__PAGE__.txt +10 -0
  86. recce/data/checks/__next.checks.txt +4 -0
  87. recce/data/checks/index.html +2 -0
  88. recce/data/checks/index.txt +39 -0
  89. recce/data/index.html +2 -27
  90. recce/data/index.txt +32 -8
  91. recce/data/lineage/__next.@lineage.__DEFAULT__.txt +7 -0
  92. recce/data/lineage/__next._full.txt +39 -0
  93. recce/data/lineage/__next._head.txt +8 -0
  94. recce/data/lineage/__next._index.txt +14 -0
  95. recce/data/lineage/__next._tree.txt +8 -0
  96. recce/data/lineage/__next.lineage.__PAGE__.txt +10 -0
  97. recce/data/lineage/__next.lineage.txt +4 -0
  98. recce/data/lineage/index.html +2 -0
  99. recce/data/lineage/index.txt +39 -0
  100. recce/data/query/__next.@lineage.__DEFAULT__.txt +7 -0
  101. recce/data/query/__next._full.txt +37 -0
  102. recce/data/query/__next._head.txt +8 -0
  103. recce/data/query/__next._index.txt +14 -0
  104. recce/data/query/__next._tree.txt +8 -0
  105. recce/data/query/__next.query.__PAGE__.txt +9 -0
  106. recce/data/query/__next.query.txt +4 -0
  107. recce/data/query/index.html +2 -0
  108. recce/data/query/index.txt +37 -0
  109. recce/event/CONFIG.bak +1 -0
  110. recce/event/__init__.py +9 -8
  111. recce/event/collector.py +6 -2
  112. recce/event/track.py +10 -0
  113. recce/github.py +1 -1
  114. recce/mcp_server.py +725 -0
  115. recce/models/check.py +433 -15
  116. recce/models/types.py +61 -2
  117. recce/pull_request.py +1 -1
  118. recce/run.py +37 -17
  119. recce/server.py +216 -21
  120. recce/state/__init__.py +31 -0
  121. recce/state/cloud.py +644 -0
  122. recce/state/const.py +26 -0
  123. recce/state/local.py +56 -0
  124. recce/state/state.py +119 -0
  125. recce/state/state_loader.py +174 -0
  126. recce/summary.py +25 -3
  127. recce/tasks/dataframe.py +63 -1
  128. recce/tasks/query.py +40 -3
  129. recce/tasks/rowcount.py +4 -1
  130. recce/tasks/schema.py +4 -1
  131. recce/tasks/utils.py +147 -0
  132. recce/tasks/valuediff.py +85 -57
  133. recce/util/api_token.py +11 -2
  134. recce/util/breaking.py +10 -1
  135. recce/util/cll.py +1 -2
  136. recce/util/cloud/__init__.py +15 -0
  137. recce/util/cloud/base.py +115 -0
  138. recce/util/cloud/check_events.py +190 -0
  139. recce/util/cloud/checks.py +242 -0
  140. recce/util/io.py +2 -2
  141. recce/util/lineage.py +19 -18
  142. recce/util/perf_tracking.py +85 -0
  143. recce/util/recce_cloud.py +254 -5
  144. recce/util/startup_perf.py +121 -0
  145. recce/yaml/__init__.py +2 -2
  146. {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/METADATA +91 -71
  147. recce_nightly-1.30.0.20251221.dist-info/RECORD +183 -0
  148. {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/WHEEL +1 -2
  149. recce/data/_next/static/abCX3x3UoIdRLEDWxx4xd/_buildManifest.js +0 -1
  150. recce/data/_next/static/chunks/181-acc61ddada3bc0ca.js +0 -43
  151. recce/data/_next/static/chunks/1bff33f1-1ef85cf5e658a751.js +0 -1
  152. recce/data/_next/static/chunks/217-879a84d70f7a907c.js +0 -2
  153. recce/data/_next/static/chunks/29e3cc0d-60045b2e47aa3916.js +0 -1
  154. recce/data/_next/static/chunks/36e1c10d-8e7be4a6c1f6ab2d.js +0 -1
  155. recce/data/_next/static/chunks/3998a672-03adacad07b346ac.js +0 -1
  156. recce/data/_next/static/chunks/3a92ee20-1081c360214f9602.js +0 -1
  157. recce/data/_next/static/chunks/42-cd3c06533f5fd47c.js +0 -9
  158. recce/data/_next/static/chunks/450c323b-fd94e7ffaa4a5efa.js +0 -1
  159. recce/data/_next/static/chunks/47d8844f-929aed9b1c73a905.js +0 -1
  160. recce/data/_next/static/chunks/608-3b079b544e5d5f5e.js +0 -15
  161. recce/data/_next/static/chunks/6dc81886-adbfa45836061d79.js +0 -1
  162. recce/data/_next/static/chunks/7a8a3e83-edf6dc64b5d5f0a5.js +0 -1
  163. recce/data/_next/static/chunks/7f27ae6c-d5f0438edd5c2a5b.js +0 -1
  164. recce/data/_next/static/chunks/86730205-cfb14e3f051bab35.js +0 -1
  165. recce/data/_next/static/chunks/8d700b6a.8bb140898499c512.js +0 -1
  166. recce/data/_next/static/chunks/92-607cd1af83c41f43.js +0 -1
  167. recce/data/_next/static/chunks/9746af58-a42b7d169cacadf0.js +0 -1
  168. recce/data/_next/static/chunks/a30376cd-de84559016d7e133.js +0 -1
  169. recce/data/_next/static/chunks/app/_not-found/page-01ed58b7f971d311.js +0 -1
  170. recce/data/_next/static/chunks/app/layout-177a410a97e0d018.js +0 -1
  171. recce/data/_next/static/chunks/app/page-da6e046a8235dbfc.js +0 -1
  172. recce/data/_next/static/chunks/b63b1b3f-4282bdcf459e075c.js +0 -1
  173. recce/data/_next/static/chunks/bbda5537-9ec25eb1dd62348a.js +0 -1
  174. recce/data/_next/static/chunks/c132bf7d-08cb668a789d6afd.js +0 -1
  175. recce/data/_next/static/chunks/ce84277d-2e5d1d46910cf052.js +0 -1
  176. recce/data/_next/static/chunks/febdd86e-c6b525341634b860.js +0 -54
  177. recce/data/_next/static/chunks/fee69bc6-2dbccaf9b90474e6.js +0 -1
  178. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  179. recce/data/_next/static/chunks/main-app-39061b0166c47f55.js +0 -1
  180. recce/data/_next/static/chunks/main-b5b3ae20a1405261.js +0 -1
  181. recce/data/_next/static/chunks/pages/_app-437c455677d62394.js +0 -1
  182. recce/data/_next/static/chunks/pages/_error-e7650df18ca04bde.js +0 -1
  183. recce/data/_next/static/chunks/webpack-7b49d5ba7e3a434d.js +0 -1
  184. recce/data/_next/static/css/17a96168e3a9db13.css +0 -1
  185. recce/data/_next/static/css/1b121dc4d36aeb4d.css +0 -3
  186. recce/data/_next/static/css/35c6679a098e1e34.css +0 -1
  187. recce/data/_next/static/css/951e2e0eea2d4a5b.css +0 -14
  188. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  189. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  190. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  191. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  192. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  193. recce/data/_next/static/media/reload-image.79aabb7d.svg +0 -4
  194. recce/state.py +0 -786
  195. recce_nightly-1.10.0.20250625.dist-info/RECORD +0 -154
  196. recce_nightly-1.10.0.20250625.dist-info/top_level.txt +0 -2
  197. tests/__init__.py +0 -0
  198. tests/adapter/__init__.py +0 -0
  199. tests/adapter/dbt_adapter/__init__.py +0 -0
  200. tests/adapter/dbt_adapter/conftest.py +0 -17
  201. tests/adapter/dbt_adapter/dbt_test_helper.py +0 -298
  202. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -25
  203. tests/adapter/dbt_adapter/test_dbt_cll.py +0 -384
  204. tests/adapter/dbt_adapter/test_selector.py +0 -202
  205. tests/tasks/__init__.py +0 -0
  206. tests/tasks/conftest.py +0 -4
  207. tests/tasks/test_histogram.py +0 -129
  208. tests/tasks/test_lineage.py +0 -55
  209. tests/tasks/test_preset_checks.py +0 -64
  210. tests/tasks/test_profile.py +0 -397
  211. tests/tasks/test_query.py +0 -151
  212. tests/tasks/test_row_count.py +0 -135
  213. tests/tasks/test_schema.py +0 -122
  214. tests/tasks/test_top_k.py +0 -77
  215. tests/tasks/test_valuediff.py +0 -85
  216. tests/test_cli.py +0 -133
  217. tests/test_config.py +0 -43
  218. tests/test_connect_to_cloud.py +0 -82
  219. tests/test_core.py +0 -29
  220. tests/test_dbt.py +0 -36
  221. tests/test_pull_request.py +0 -130
  222. tests/test_server.py +0 -104
  223. tests/test_state.py +0 -134
  224. tests/test_summary.py +0 -65
  225. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  226. /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
  227. /recce/data/_next/static/{abCX3x3UoIdRLEDWxx4xd → nX-Uz0AH6Tc6hIQUFGqaB}/_ssgManifest.js +0 -0
  228. {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/entry_points.txt +0 -0
  229. {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/licenses/LICENSE +0 -0
recce/apis/check_api.py CHANGED
@@ -1,4 +1,3 @@
1
- from datetime import datetime, timezone
2
1
  from typing import Optional
3
2
  from uuid import UUID
4
3
 
@@ -153,22 +152,10 @@ class PatchCheckIn(BaseModel):
153
152
 
154
153
  @check_router.patch("/checks/{check_id}", status_code=200, response_model=CheckOut, response_model_exclude_none=True)
155
154
  async def update_check_handler(check_id: UUID, patch: PatchCheckIn, background_tasks: BackgroundTasks):
156
- check = CheckDAO().find_check_by_id(check_id)
155
+ check = CheckDAO().update_check_by_id(check_id, patch)
157
156
  if check is None:
158
157
  raise HTTPException(status_code=404, detail="Not Found")
159
158
 
160
- if patch.name is not None:
161
- check.name = patch.name
162
- if patch.description is not None:
163
- check.description = patch.description
164
- if patch.params is not None:
165
- check.params = patch.params
166
- if patch.view_options is not None:
167
- check.view_options = patch.view_options
168
- if patch.is_checked is not None:
169
- check.is_checked = patch.is_checked
170
- check.updated_at = datetime.now(timezone.utc).replace(microsecond=0)
171
-
172
159
  background_tasks.add_task(export_persistent_state)
173
160
  return CheckOut.from_check(check)
174
161
 
@@ -195,3 +182,22 @@ async def reorder_handler(order: ReorderChecksIn):
195
182
  CheckDAO().reorder(order.source, order.destination)
196
183
  except RecceException as e:
197
184
  raise HTTPException(status_code=400, detail=e.message)
185
+
186
+
187
+ @check_router.post("/checks/{check_id}/mark-as-preset", status_code=204)
188
+ async def mark_as_preset_check_handler(check_id: UUID, background_tasks: BackgroundTasks):
189
+ """
190
+ Mark an existing check as a preset check (cloud users only).
191
+
192
+ This creates a preset check from the specified check.
193
+ Only available for users with cloud mode enabled.
194
+
195
+ Returns:
196
+ 204 No Content: Successfully marked check as preset
197
+ 400 Bad Request: Error with detail message (e.g., not in cloud mode, check not found)
198
+ """
199
+ try:
200
+ CheckDAO().mark_as_preset_check(check_id)
201
+ background_tasks.add_task(export_persistent_state)
202
+ except RecceException as e:
203
+ raise HTTPException(status_code=400, detail=e.message)
@@ -0,0 +1,353 @@
1
+ """
2
+ Check Events API endpoints.
3
+
4
+ This module provides REST endpoints for check events (timeline/conversation),
5
+ proxying requests to Recce Cloud. This feature is only available for cloud users.
6
+ """
7
+
8
+ import logging
9
+ from typing import List, Optional
10
+ from uuid import UUID
11
+
12
+ from fastapi import APIRouter, HTTPException
13
+ from pydantic import BaseModel
14
+
15
+ from recce.core import default_context
16
+ from recce.event import get_recce_api_token
17
+ from recce.exceptions import RecceException
18
+ from recce.util.cloud.check_events import CheckEventsCloud
19
+ from recce.util.recce_cloud import RecceCloud, RecceCloudException
20
+
21
+ logger = logging.getLogger("uvicorn")
22
+
23
+ check_events_router = APIRouter(tags=["check_events"])
24
+
25
+
26
+ # ============================================================================
27
+ # Helper Functions
28
+ # ============================================================================
29
+
30
+
31
+ def _is_cloud_user() -> bool:
32
+ """Check if the current user is connected to Recce Cloud."""
33
+ ctx = default_context()
34
+ if ctx is None or ctx.state_loader is None:
35
+ return False
36
+ return hasattr(ctx.state_loader, "session_id") and ctx.state_loader.session_id is not None
37
+
38
+
39
+ def _get_session_info() -> tuple:
40
+ """
41
+ Get organization ID, project ID, and session ID from state loader.
42
+
43
+ Returns:
44
+ tuple: (org_id, project_id, session_id)
45
+
46
+ Raises:
47
+ HTTPException: If not in cloud mode or session info unavailable
48
+ """
49
+ if not _is_cloud_user():
50
+ raise HTTPException(
51
+ status_code=400,
52
+ detail="Check events are only available when connected to Recce Cloud.",
53
+ )
54
+
55
+ ctx = default_context()
56
+ state_loader = ctx.state_loader
57
+
58
+ session_id = state_loader.session_id
59
+
60
+ # Check if org_id and project_id are cached
61
+ if hasattr(state_loader, "org_id") and hasattr(state_loader, "project_id"):
62
+ return state_loader.org_id, state_loader.project_id, session_id
63
+
64
+ # Fetch from cloud API
65
+ api_token = get_recce_api_token() or state_loader.token
66
+ if not api_token:
67
+ raise HTTPException(
68
+ status_code=401,
69
+ detail="Cannot access Recce Cloud: no API token available.",
70
+ )
71
+
72
+ try:
73
+ recce_cloud = RecceCloud(api_token)
74
+ session = recce_cloud.get_session(session_id)
75
+
76
+ org_id = session.get("org_id")
77
+ project_id = session.get("project_id")
78
+
79
+ if not org_id or not project_id:
80
+ raise HTTPException(
81
+ status_code=400,
82
+ detail=f"Session {session_id} does not belong to a valid organization or project.",
83
+ )
84
+
85
+ # Cache for future use
86
+ state_loader.org_id = org_id
87
+ state_loader.project_id = project_id
88
+
89
+ return org_id, project_id, session_id
90
+
91
+ except RecceCloudException as e:
92
+ logger.error(f"Failed to get session info: {e}")
93
+ raise HTTPException(status_code=e.status_code, detail=str(e.reason))
94
+
95
+
96
+ def _get_events_client() -> CheckEventsCloud:
97
+ """
98
+ Get the CheckEventsCloud client.
99
+
100
+ Returns:
101
+ CheckEventsCloud: Cloud client for event operations
102
+
103
+ Raises:
104
+ HTTPException: If client cannot be initialized
105
+ """
106
+ ctx = default_context()
107
+ api_token = get_recce_api_token() or ctx.state_loader.token
108
+
109
+ if not api_token:
110
+ raise HTTPException(
111
+ status_code=401,
112
+ detail="Cannot access Recce Cloud: no API token available.",
113
+ )
114
+
115
+ return CheckEventsCloud(api_token)
116
+
117
+
118
+ # ============================================================================
119
+ # Pydantic Models
120
+ # ============================================================================
121
+
122
+
123
+ class CheckEventActorOut(BaseModel):
124
+ """Actor who performed the event."""
125
+
126
+ type: str # "user", "recce_ai", "preset_system"
127
+ user_id: Optional[int] = None
128
+ login: Optional[str] = None
129
+ fullname: Optional[str] = None
130
+
131
+
132
+ class CheckEventOut(BaseModel):
133
+ """Check event response model."""
134
+
135
+ id: str
136
+ check_id: str
137
+ event_type: str
138
+ actor: CheckEventActorOut
139
+ content: Optional[str] = None
140
+ old_value: Optional[str] = None
141
+ new_value: Optional[str] = None
142
+ is_edited: bool = False
143
+ is_deleted: bool = False
144
+ created_at: str
145
+ updated_at: str
146
+
147
+
148
+ class CreateCommentIn(BaseModel):
149
+ """Request body for creating a comment."""
150
+
151
+ content: str
152
+
153
+
154
+ class UpdateCommentIn(BaseModel):
155
+ """Request body for updating a comment."""
156
+
157
+ content: str
158
+
159
+
160
+ # ============================================================================
161
+ # API Endpoints
162
+ # ============================================================================
163
+
164
+
165
+ @check_events_router.get(
166
+ "/checks/{check_id}/events",
167
+ status_code=200,
168
+ response_model=List[CheckEventOut],
169
+ )
170
+ async def list_check_events(check_id: UUID):
171
+ """
172
+ List all events for a check in chronological order.
173
+
174
+ This endpoint returns all events (comments, state changes, etc.) for the
175
+ specified check. Events are returned in chronological order (oldest first).
176
+
177
+ Args:
178
+ check_id: The check ID
179
+
180
+ Returns:
181
+ List of CheckEventOut objects
182
+
183
+ Raises:
184
+ 400: Not connected to Recce Cloud
185
+ 401: No API token available
186
+ 404: Check not found
187
+ """
188
+ try:
189
+ org_id, project_id, session_id = _get_session_info()
190
+ client = _get_events_client()
191
+
192
+ events = client.list_events(org_id, project_id, session_id, str(check_id))
193
+ return events
194
+
195
+ except RecceCloudException as e:
196
+ logger.error(f"Failed to list check events: {e}")
197
+ raise HTTPException(status_code=e.status_code, detail=str(e.reason))
198
+ except RecceException as e:
199
+ logger.error(f"Failed to list check events: {e}")
200
+ raise HTTPException(status_code=400, detail=str(e))
201
+
202
+
203
+ @check_events_router.get(
204
+ "/checks/{check_id}/events/{event_id}",
205
+ status_code=200,
206
+ response_model=CheckEventOut,
207
+ )
208
+ async def get_check_event(check_id: UUID, event_id: UUID):
209
+ """
210
+ Get a specific event by ID.
211
+
212
+ Args:
213
+ check_id: The check ID
214
+ event_id: The event ID
215
+
216
+ Returns:
217
+ CheckEventOut object
218
+
219
+ Raises:
220
+ 400: Not connected to Recce Cloud
221
+ 401: No API token available
222
+ 404: Event not found
223
+ """
224
+ try:
225
+ org_id, project_id, session_id = _get_session_info()
226
+ client = _get_events_client()
227
+
228
+ event = client.get_event(org_id, project_id, session_id, str(check_id), str(event_id))
229
+ return event
230
+
231
+ except RecceCloudException as e:
232
+ logger.error(f"Failed to get check event: {e}")
233
+ raise HTTPException(status_code=e.status_code, detail=str(e.reason))
234
+ except RecceException as e:
235
+ logger.error(f"Failed to get check event: {e}")
236
+ raise HTTPException(status_code=400, detail=str(e))
237
+
238
+
239
+ @check_events_router.post(
240
+ "/checks/{check_id}/events",
241
+ status_code=201,
242
+ response_model=CheckEventOut,
243
+ )
244
+ async def create_comment(check_id: UUID, body: CreateCommentIn):
245
+ """
246
+ Create a new comment on a check.
247
+
248
+ Args:
249
+ check_id: The check ID
250
+ body: Request body containing comment content
251
+
252
+ Returns:
253
+ Created CheckEventOut object
254
+
255
+ Raises:
256
+ 400: Not connected to Recce Cloud or invalid content
257
+ 401: No API token available
258
+ 404: Check not found
259
+ """
260
+ if not body.content or not body.content.strip():
261
+ raise HTTPException(status_code=400, detail="Comment content cannot be empty.")
262
+
263
+ try:
264
+ org_id, project_id, session_id = _get_session_info()
265
+ client = _get_events_client()
266
+
267
+ event = client.create_comment(org_id, project_id, session_id, str(check_id), body.content)
268
+ return event
269
+
270
+ except RecceCloudException as e:
271
+ logger.error(f"Failed to create comment: {e}")
272
+ raise HTTPException(status_code=e.status_code, detail=str(e.reason))
273
+ except RecceException as e:
274
+ logger.error(f"Failed to create comment: {e}")
275
+ raise HTTPException(status_code=400, detail=str(e))
276
+
277
+
278
+ @check_events_router.patch(
279
+ "/checks/{check_id}/events/{event_id}",
280
+ status_code=200,
281
+ response_model=CheckEventOut,
282
+ )
283
+ async def update_comment(check_id: UUID, event_id: UUID, body: UpdateCommentIn):
284
+ """
285
+ Update an existing comment.
286
+
287
+ Only the author or an admin can update a comment.
288
+
289
+ Args:
290
+ check_id: The check ID
291
+ event_id: The event ID of the comment to update
292
+ body: Request body containing new comment content
293
+
294
+ Returns:
295
+ Updated CheckEventOut object
296
+
297
+ Raises:
298
+ 400: Not connected to Recce Cloud or invalid content
299
+ 401: No API token available
300
+ 403: Not authorized to update this comment
301
+ 404: Comment not found
302
+ """
303
+ if not body.content or not body.content.strip():
304
+ raise HTTPException(status_code=400, detail="Comment content cannot be empty.")
305
+
306
+ try:
307
+ org_id, project_id, session_id = _get_session_info()
308
+ client = _get_events_client()
309
+
310
+ event = client.update_comment(org_id, project_id, session_id, str(check_id), str(event_id), body.content)
311
+ return event
312
+
313
+ except RecceCloudException as e:
314
+ logger.error(f"Failed to update comment: {e}")
315
+ raise HTTPException(status_code=e.status_code, detail=str(e.reason))
316
+ except RecceException as e:
317
+ logger.error(f"Failed to update comment: {e}")
318
+ raise HTTPException(status_code=400, detail=str(e))
319
+
320
+
321
+ @check_events_router.delete(
322
+ "/checks/{check_id}/events/{event_id}",
323
+ status_code=204,
324
+ )
325
+ async def delete_comment(check_id: UUID, event_id: UUID):
326
+ """
327
+ Delete a comment (soft delete).
328
+
329
+ Only the author or an admin can delete a comment. The comment will be
330
+ marked as deleted but remain in the timeline with a "Comment deleted" indicator.
331
+
332
+ Args:
333
+ check_id: The check ID
334
+ event_id: The event ID of the comment to delete
335
+
336
+ Raises:
337
+ 400: Not connected to Recce Cloud
338
+ 401: No API token available
339
+ 403: Not authorized to delete this comment
340
+ 404: Comment not found
341
+ """
342
+ try:
343
+ org_id, project_id, session_id = _get_session_info()
344
+ client = _get_events_client()
345
+
346
+ client.delete_comment(org_id, project_id, session_id, str(check_id), str(event_id))
347
+
348
+ except RecceCloudException as e:
349
+ logger.error(f"Failed to delete comment: {e}")
350
+ raise HTTPException(status_code=e.status_code, detail=str(e.reason))
351
+ except RecceException as e:
352
+ logger.error(f"Failed to delete comment: {e}")
353
+ raise HTTPException(status_code=400, detail=str(e))
recce/apis/check_func.py CHANGED
@@ -86,10 +86,10 @@ def create_check_from_run(
86
86
  is_preset=is_preset,
87
87
  is_checked=is_checked,
88
88
  )
89
- CheckDAO().create(check)
90
- run.check_id = check.check_id
89
+ new_check = CheckDAO().create(check)
90
+ run.check_id = new_check.check_id
91
91
 
92
- return check
92
+ return new_check
93
93
 
94
94
 
95
95
  def create_check_without_run(
@@ -105,8 +105,8 @@ def create_check_without_run(
105
105
  is_preset=is_preset,
106
106
  is_checked=is_checked,
107
107
  )
108
- CheckDAO().create(check)
109
- return check
108
+ new_check = CheckDAO().create(check)
109
+ return new_check
110
110
 
111
111
 
112
112
  def purge_preset_checks():
recce/apis/run_func.py CHANGED
@@ -122,12 +122,16 @@ def submit_run(type, params, check_id=None):
122
122
 
123
123
  task.progress_listener = progress_listener
124
124
 
125
- 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."""
126
127
  if run is None:
127
128
  return
128
129
  if result is not None:
129
130
  run.result = result
130
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)
131
135
  if error is not None:
132
136
  failed_reason = str(error) if str(error) != "None" else repr(error)
133
137
  run.error = failed_reason
@@ -138,10 +142,35 @@ def submit_run(type, params, check_id=None):
138
142
  def fn():
139
143
  try:
140
144
  result = task.execute()
141
- 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)
142
171
  return result
143
172
  except BaseException as e:
144
- 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)
145
174
  if isinstance(e, RecceException) and e.is_raise is False:
146
175
  return None
147
176
  import sentry_sdk
recce/artifact.py CHANGED
@@ -40,7 +40,7 @@ def verify_artifacts_path(target_path: str) -> bool:
40
40
 
41
41
 
42
42
  def parse_dbt_version(file_path: str) -> str:
43
- with open(file_path, "r") as f:
43
+ with open(file_path, "r", encoding="utf-8") as f:
44
44
  data = json.load(f)
45
45
 
46
46
  dbt_version = data.get("metadata", {}).get("dbt_version", None)
@@ -80,6 +80,64 @@ def archive_artifacts(target_path: str) -> (str, str):
80
80
  return artifacts_tar_gz_path, dbt_version
81
81
 
82
82
 
83
+ def upload_artifacts_to_session(target_path: str, session_id: str, token: str, debug: bool = False):
84
+ """Upload dbt artifacts to a specific session ID in Recce Cloud."""
85
+ console = Console()
86
+ if verify_artifacts_path(target_path) is False:
87
+ console.print(f"[[red]Error[/red]] Invalid target path: {target_path}")
88
+ console.print("Please provide a valid target path containing manifest.json and catalog.json.")
89
+ return 1
90
+
91
+ manifest_path = os.path.join(target_path, "manifest.json")
92
+ catalog_path = os.path.join(target_path, "catalog.json")
93
+
94
+ # get the adapter type from the manifest file
95
+ with open(manifest_path, "r", encoding="utf-8") as f:
96
+ manifest_data = json.load(f)
97
+ adapter_type = manifest_data.get("metadata", {}).get("adapter_type")
98
+ if adapter_type is None:
99
+ raise Exception("Failed to parse adapter type from manifest.json")
100
+
101
+ recce_cloud = RecceCloud(token)
102
+
103
+ session = recce_cloud.get_session(session_id)
104
+
105
+ org_id = session.get("org_id")
106
+ if org_id is None:
107
+ raise Exception(f"Session ID {session_id} does not belong to any organization.")
108
+
109
+ project_id = session.get("project_id")
110
+ if project_id is None:
111
+ raise Exception(f"Session ID {session_id} does not belong to any project.")
112
+
113
+ # Get the presigned URL for uploading the artifacts using session ID
114
+ console.print(f'Uploading artifacts for session ID "{session_id}"')
115
+ presigned_urls = recce_cloud.get_upload_urls_by_session_id(org_id, project_id, session_id)
116
+ if debug:
117
+ console.rule("Debug information", style="blue")
118
+ console.print(f"Org ID: {org_id}")
119
+ console.print(f"Project ID: {project_id}")
120
+ console.print(f"Session ID: {session_id}")
121
+ console.print(f"Manifest path: {presigned_urls['manifest_url']}")
122
+ console.print(f"Catalog path: {presigned_urls['catalog_url']}")
123
+ console.print(f"Adapter type: {adapter_type}")
124
+
125
+ # Upload the compressed artifacts (no password needed for session uploads)
126
+ console.print(f'Uploading manifest from path "{manifest_path}"')
127
+ response = requests.put(presigned_urls["manifest_url"], data=open(manifest_path, "rb").read())
128
+ if response.status_code != 200 and response.status_code != 204:
129
+ raise Exception(response.text)
130
+ console.print(f'Uploading catalog from path "{catalog_path}"')
131
+ response = requests.put(presigned_urls["catalog_url"], data=open(catalog_path, "rb").read())
132
+ if response.status_code != 200 and response.status_code != 204:
133
+ raise Exception(response.text)
134
+
135
+ # Update the session metadata
136
+ recce_cloud.update_session(org_id, project_id, session_id, adapter_type)
137
+
138
+ return 0
139
+
140
+
83
141
  def upload_dbt_artifacts(target_path: str, branch: str, token: str, password: str, debug: bool = False):
84
142
  console = Console()
85
143
  if verify_artifacts_path(target_path) is False:
@@ -100,7 +158,7 @@ def upload_dbt_artifacts(target_path: str, branch: str, token: str, password: st
100
158
  metadata = {"commit": sha, "dbt_version": dbt_version}
101
159
 
102
160
  # Get the presigned URL for uploading the artifacts
103
- presigned_url = RecceCloud(token).get_presigned_url(
161
+ presigned_url = RecceCloud(token).get_presigned_url_by_github_repo(
104
162
  method=PresignedUrlMethod.UPLOAD,
105
163
  repository=repo,
106
164
  artifact_name="dbt_artifacts.tar.gz",
@@ -145,7 +203,7 @@ def download_dbt_artifacts(
145
203
  sha = None
146
204
  dbt_version = None
147
205
 
148
- presigned_url, tags = RecceCloud(token).get_download_presigned_url_with_tags(
206
+ presigned_url, tags = RecceCloud(token).get_download_presigned_url_by_github_repo_with_tags(
149
207
  repository=repo,
150
208
  artifact_name="dbt_artifacts.tar.gz",
151
209
  branch=branch,
@@ -191,3 +249,18 @@ def download_dbt_artifacts(
191
249
  except FileNotFoundError:
192
250
  pass
193
251
  return 0
252
+
253
+
254
+ def delete_dbt_artifacts(branch: str, token: str, debug: bool = False):
255
+ """Delete dbt artifacts from a specific branch in Recce Cloud."""
256
+ console = Console()
257
+ repo = hosting_repo()
258
+
259
+ if debug:
260
+ console.rule("Debug information", style="blue")
261
+ console.print(f"Git Branch: {branch}")
262
+ console.print(f"GitHub repository: {repo}")
263
+
264
+ console.print(f'Deleting dbt artifacts from branch: "{branch}"')
265
+
266
+ RecceCloud(token).purge_artifacts(repo, branch=branch)