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/models/check.py CHANGED
@@ -1,43 +1,394 @@
1
+ """
2
+ CheckDAO with cloud integration.
3
+
4
+ This module provides data access for Check objects with support for both
5
+ local (in-memory) and cloud (Recce Cloud API) storage modes.
6
+ """
7
+
8
+ import logging
9
+ import typing
10
+ from datetime import datetime, timezone
1
11
  from typing import List, Optional
12
+ from uuid import UUID
2
13
 
3
14
  from recce.exceptions import RecceException
4
15
 
5
- from .types import Check
16
+ from .types import Check, RunType
17
+
18
+ if typing.TYPE_CHECKING:
19
+ from ..apis.check_api import PatchCheckIn
20
+
21
+ logger = logging.getLogger("uvicorn")
6
22
 
7
23
 
8
24
  class CheckDAO:
9
25
  """
10
- Data Access Object for Check. Currently, we store runs in memory, in the future, we can store them in a database.
26
+ Data Access Object for Check.
27
+
28
+ Supports two modes:
29
+ - Local mode: Stores checks in memory
30
+ - Cloud mode: Stores checks in Recce Cloud via API
31
+
32
+ The mode is determined by checking if a session_id exists in the state_loader.
11
33
  """
12
34
 
35
+ def __init__(self):
36
+ """Initialize CheckDAO."""
37
+ self._session_info_cache = None
38
+
13
39
  @property
14
40
  def _checks(self):
41
+ """Get checks from local context."""
15
42
  from recce.core import default_context
16
43
 
17
44
  return default_context().checks
18
45
 
19
- def create(self, check) -> None:
20
- self._checks.append(check)
46
+ @property
47
+ def is_cloud_user(self) -> bool:
48
+ """
49
+ Determine if the user is in cloud mode.
50
+
51
+ Returns True if state_loader has a session_id, indicating cloud mode.
52
+ Returns False otherwise, indicating local mode.
53
+
54
+ Returns:
55
+ bool: True if cloud mode, False if local mode
56
+ """
57
+ from recce.core import default_context
58
+
59
+ ctx = default_context()
60
+ if ctx is None or ctx.state_loader is None:
61
+ return False
62
+
63
+ return hasattr(ctx.state_loader, "session_id") and ctx.state_loader.session_id is not None
64
+
65
+ def _get_session_info(self) -> tuple[str, str, str]:
66
+ """
67
+ Get organization ID, project ID, and session ID from state loader.
68
+
69
+ Caches the session info to avoid repeated API calls.
70
+
71
+ Returns:
72
+ tuple: (org_id, project_id, session_id)
73
+
74
+ Raises:
75
+ RecceException: If session info cannot be retrieved
76
+ """
77
+ from recce.core import default_context
78
+
79
+ if self._session_info_cache is not None:
80
+ return self._session_info_cache
81
+
82
+ ctx = default_context()
83
+ state_loader = ctx.state_loader
84
+
85
+ if not hasattr(state_loader, "session_id") or state_loader.session_id is None:
86
+ raise RecceException("Cannot get session info: no session_id in state_loader")
87
+
88
+ session_id = state_loader.session_id
89
+
90
+ # Get org_id and project_id from the session
91
+ # First check if they're already cached on the state_loader
92
+ if hasattr(state_loader, "org_id") and hasattr(state_loader, "project_id"):
93
+ org_id = state_loader.org_id
94
+ project_id = state_loader.project_id
95
+ else:
96
+ # Fetch from cloud API
97
+ from recce.event import get_recce_api_token
98
+ from recce.util.recce_cloud import RecceCloud
99
+
100
+ api_token = get_recce_api_token() or ctx.state_loader.token
101
+ if not api_token:
102
+ raise RecceException("Cannot access Recce Cloud: no API token available")
103
+
104
+ recce_cloud = RecceCloud(api_token)
105
+ session = recce_cloud.get_session(session_id)
106
+
107
+ org_id = session.get("org_id")
108
+ project_id = session.get("project_id")
109
+
110
+ if not org_id or not project_id:
111
+ raise RecceException(f"Session {session_id} does not belong to a valid organization or project")
112
+
113
+ # Cache on state_loader for future use
114
+ state_loader.org_id = org_id
115
+ state_loader.project_id = project_id
116
+
117
+ self._session_info_cache = (org_id, project_id, session_id)
118
+ return self._session_info_cache
119
+
120
+ @staticmethod
121
+ def _get_cloud_client():
122
+ """
123
+ Get the cloud checks client.
124
+
125
+ Returns:
126
+ ChecksCloud: Cloud client for check operations
127
+
128
+ Raises:
129
+ RecceException: If cloud client cannot be initialized
130
+ """
131
+ from recce.core import default_context
132
+ from recce.event import get_recce_api_token
133
+ from recce.util.recce_cloud import RecceCloud
134
+
135
+ ctx = default_context()
136
+
137
+ api_token = get_recce_api_token() or ctx.state_loader.token
138
+ if not api_token:
139
+ raise RecceException("Cannot access Recce Cloud: no API token available")
140
+
141
+ recce_cloud = RecceCloud(api_token)
142
+ return recce_cloud.checks
143
+
144
+ @staticmethod
145
+ def _check_to_cloud_format(check: Check) -> dict:
146
+ """
147
+ Convert a Check object to cloud API format.
148
+
149
+ Args:
150
+ check: Check object to convert
151
+
152
+ Returns:
153
+ dict: Check data in cloud API format
154
+ """
155
+ return {
156
+ "session_id": str(check.session_id) if check.session_id else None,
157
+ "name": check.name,
158
+ "type": check.type.value,
159
+ "params": check.params or {},
160
+ "created_by": check.created_by,
161
+ "description": check.description,
162
+ "view_options": check.view_options or {},
163
+ "is_checked": check.is_checked,
164
+ "is_preset": check.is_preset,
165
+ "updated_by": check.updated_by,
166
+ "created_at": check.created_at.isoformat() if check.created_at else None,
167
+ "updated_at": check.updated_at.isoformat() if check.updated_at else None,
168
+ }
169
+
170
+ @staticmethod
171
+ def _cloud_to_check(cloud_data: dict) -> Check:
172
+ """
173
+ Convert cloud API data to a Check object.
174
+
175
+ Args:
176
+ cloud_data: Check data from cloud API
177
+
178
+ Returns:
179
+ Check: Check object
180
+ """
181
+
182
+ logger.debug(f"Converting cloud data to Check object for check: {cloud_data.get('id')}")
183
+ # Parse the type
184
+ check_type = RunType(cloud_data.get("type"))
185
+
186
+ return Check(
187
+ check_id=UUID(cloud_data.get("id")),
188
+ session_id=UUID(cloud_data.get("session_id")),
189
+ name=cloud_data.get("name"),
190
+ description=cloud_data.get("description", ""),
191
+ type=check_type,
192
+ params=cloud_data.get("params", {}),
193
+ view_options=cloud_data.get("view_options", {}),
194
+ is_checked=cloud_data.get("is_checked", False),
195
+ is_preset=cloud_data.get("is_preset", False),
196
+ created_by=(cloud_data.get("created_by") or {}).get("email", ""),
197
+ updated_by=(cloud_data.get("updated_by") or {}).get("email", ""),
198
+ created_at=datetime.fromisoformat(cloud_data["created_at"]) if cloud_data.get("created_at") else None,
199
+ updated_at=datetime.fromisoformat(cloud_data["updated_at"]) if cloud_data.get("updated_at") else None,
200
+ )
201
+
202
+ def create(self, check: Check) -> Check:
203
+ """
204
+ Create a new check.
205
+
206
+ In local mode: Appends check to in-memory list
207
+ In cloud mode: Creates check via Recce Cloud API
208
+
209
+ Args:
210
+ check: Check object to create
211
+
212
+ Raises:
213
+ RecceException: If creation fails in cloud mode
214
+ """
215
+ if self.is_cloud_user:
216
+ try:
217
+ org_id, project_id, session_id = self._get_session_info()
218
+ cloud_client = self._get_cloud_client()
219
+
220
+ check_data = self._check_to_cloud_format(check)
221
+ cloud_check = cloud_client.create_check(org_id, project_id, session_id, check_data)
222
+ new_check = self._cloud_to_check(cloud_check)
223
+
224
+ logger.debug(f"Created check {new_check.check_id} in cloud")
225
+ return new_check
226
+ except Exception as e:
227
+ logger.error(f"Failed to create check in cloud: {e}")
228
+ raise RecceException(f"Failed to create check in Recce Cloud: {e}")
229
+ else:
230
+ # Local mode
231
+ self._checks.append(check)
232
+ return check
21
233
 
22
234
  def find_check_by_id(self, check_id) -> Optional[Check]:
23
- for check in self._checks:
24
- if str(check_id) == str(check.check_id):
25
- return check
235
+ """
236
+ Find a check by its ID.
237
+
238
+ In local mode: Searches in-memory list
239
+ In cloud mode: Retrieves check from Recce Cloud API
240
+
241
+ Args:
242
+ check_id: Check ID (UUID or string)
243
+
244
+ Returns:
245
+ Check object if found, None otherwise
246
+ """
247
+ if self.is_cloud_user:
248
+ try:
249
+ org_id, project_id, session_id = self._get_session_info()
250
+ cloud_client = self._get_cloud_client()
251
+
252
+ cloud_data = cloud_client.get_check(org_id, project_id, session_id, str(check_id))
253
+ return self._cloud_to_check(cloud_data)
254
+ except Exception as e:
255
+ logger.error(f"Failed to get check {check_id} from cloud: {e}")
256
+ return None
257
+ else:
258
+ # Local mode
259
+ for check in self._checks:
260
+ if str(check_id) == str(check.check_id):
261
+ return check
262
+ return None
26
263
 
27
- return None
264
+ def update_check_by_id(self, check_id, patch: "PatchCheckIn") -> Optional[Check]:
265
+ """
266
+ Update a check by its ID.
267
+
268
+ In local mode: Updates in-memory list
269
+ In cloud mode: Updates via Recce Cloud API
270
+
271
+ Args:
272
+ check_id: Check ID (UUID or string)
273
+ patch: Partial Check object with updated data
274
+
275
+ Returns:
276
+ bool: True if updated, False if not found
277
+ """
278
+ if self.is_cloud_user:
279
+ try:
280
+ org_id, project_id, session_id = self._get_session_info()
281
+ cloud_client = self._get_cloud_client()
282
+
283
+ # Directly send the patch object to the cloud API
284
+ cloud_data = cloud_client.update_check(
285
+ org_id, project_id, session_id, str(check_id), patch.model_dump(exclude_unset=True)
286
+ )
287
+
288
+ logger.debug(f"Updated check {check_id} in cloud")
289
+ return self._cloud_to_check(cloud_data)
290
+ except Exception as e:
291
+ logger.error(f"Failed to update check {check_id} in cloud: {e}")
292
+ return None
293
+ else:
294
+ # Local mode
295
+ check = CheckDAO().find_check_by_id(check_id)
296
+ if check is None:
297
+ return None
298
+
299
+ if patch.name is not None:
300
+ check.name = patch.name
301
+ if patch.description is not None:
302
+ check.description = patch.description
303
+ if patch.params is not None:
304
+ check.params = patch.params
305
+ if patch.view_options is not None:
306
+ check.view_options = patch.view_options
307
+ if patch.is_checked is not None:
308
+ check.is_checked = patch.is_checked
309
+ check.updated_at = datetime.now(timezone.utc).replace(microsecond=0)
310
+
311
+ return check
28
312
 
29
313
  def delete(self, check_id) -> bool:
30
- for check in self._checks:
31
- if str(check_id) == str(check.check_id):
32
- self._checks.remove(check)
33
- return True
314
+ """
315
+ Delete a check by its ID.
316
+
317
+ In local mode: Removes from in-memory list
318
+ In cloud mode: Deletes via Recce Cloud API
319
+
320
+ Args:
321
+ check_id: Check ID (UUID or string)
34
322
 
35
- return False
323
+ Returns:
324
+ bool: True if deleted, False if not found
325
+ """
326
+ if self.is_cloud_user:
327
+ try:
328
+ org_id, project_id, session_id = self._get_session_info()
329
+ cloud_client = self._get_cloud_client()
330
+
331
+ cloud_client.delete_check(org_id, project_id, session_id, str(check_id))
332
+ logger.debug(f"Deleted check {check_id} from cloud")
333
+ return True
334
+ except Exception as e:
335
+ logger.error(f"Failed to delete check {check_id} from cloud: {e}")
336
+ return False
337
+ else:
338
+ # Local mode
339
+ for check in self._checks:
340
+ if str(check_id) == str(check.check_id):
341
+ self._checks.remove(check)
342
+ return True
343
+ return False
36
344
 
37
345
  def list(self) -> List[Check]:
38
- return list(self._checks)
346
+ """
347
+ List all checks.
348
+
349
+ In local mode: Returns copy of in-memory list
350
+ In cloud mode: Retrieves all checks from Recce Cloud API
351
+
352
+ Returns:
353
+ List of Check objects
354
+ """
355
+ if self.is_cloud_user:
356
+ try:
357
+ org_id, project_id, session_id = self._get_session_info()
358
+ logger.debug(f"Listing checks from cloud: {org_id}:{project_id}:{session_id}")
359
+ cloud_client = self._get_cloud_client()
360
+
361
+ cloud_checks = cloud_client.list_checks(org_id, project_id, session_id)
362
+ return [self._cloud_to_check(check_data) for check_data in cloud_checks]
363
+ except AttributeError as e:
364
+ logger.error(f"Attribute error while listing checks from cloud: {e}")
365
+ return []
366
+ except Exception as e:
367
+ logger.exception(e)
368
+ # Return empty list on error to avoid breaking the UI
369
+ return []
370
+ else:
371
+ # Local mode
372
+ return list(self._checks)
39
373
 
40
374
  def reorder(self, source: int, destination: int):
375
+ """
376
+ Reorder checks.
377
+
378
+ Note: This operation is only supported in local mode.
379
+ In cloud mode, raises an exception as reordering must be handled server-side.
380
+
381
+ Args:
382
+ source: Source index
383
+ destination: Destination index
384
+
385
+ Raises:
386
+ RecceException: If indices are out of range or if in cloud mode
387
+ """
388
+ if self.is_cloud_user:
389
+ raise RecceException(
390
+ "Reordering checks is not supported in cloud mode. " "Check order is managed server-side."
391
+ )
41
392
 
42
393
  if source < 0 or source >= len(self._checks):
43
394
  raise RecceException("Failed to reorder checks. Source index out of range")
@@ -49,7 +400,74 @@ class CheckDAO:
49
400
  self._checks.insert(destination, check_to_move)
50
401
 
51
402
  def clear(self):
403
+ """
404
+ Clear all checks.
405
+
406
+ Note: This operation is only supported in local mode.
407
+ In cloud mode, this is a no-op with a warning.
408
+ """
409
+ if self.is_cloud_user:
410
+ logger.warning("Clear operation is not supported in cloud mode")
411
+ return
412
+
52
413
  self._checks.clear()
53
414
 
415
+ def mark_as_preset_check(self, check_id: UUID, order_idx: int = 0) -> None:
416
+ """
417
+ Mark a check as a preset check.
418
+
419
+ This operation is only supported for cloud users. It creates a preset check
420
+ from an existing check, which can then be used across projects.
421
+
422
+ Args:
423
+ check_id: Check ID (UUID)
424
+ order_idx: Order index for the preset check (default: 0)
425
+
426
+ Returns:
427
+ None
428
+
429
+ Raises:
430
+ RecceException: If operation is attempted in local mode or if check not found
431
+ """
432
+ if not self.is_cloud_user:
433
+ raise RecceException(
434
+ "Marking checks as preset is only supported in cloud mode. This feature requires Recce Cloud."
435
+ )
436
+
437
+ # Get the original check
438
+ check = self.find_check_by_id(check_id)
439
+ if check is None:
440
+ raise RecceException(f"Check {check_id} not found")
441
+
442
+ try:
443
+ org_id, project_id, session_id = self._get_session_info()
444
+ cloud_client = self._get_cloud_client()
445
+
446
+ # Prepare preset check data
447
+ preset_data = {
448
+ "name": check.name,
449
+ "description": check.description if check.description else None,
450
+ "type": check.type.value,
451
+ "params": check.params if check.params else {},
452
+ "view_options": check.view_options if check.view_options else None,
453
+ "order_index": order_idx, # Order index for the preset check
454
+ "check_id": str(check_id),
455
+ }
456
+
457
+ # Create preset check via cloud API
458
+ cloud_client.create_preset_check(org_id, project_id, preset_data)
459
+
460
+ logger.debug(f"Created preset check from check {check_id}")
461
+ except Exception as e:
462
+ logger.error(f"Failed to mark check {check_id} as preset: {e}")
463
+ raise RecceException(f"Failed to create preset check: {e}")
464
+
54
465
  def status(self):
55
- return {"total": len(self._checks), "approved": len([c for c in self._checks if c.is_checked])}
466
+ """
467
+ Get check statistics.
468
+
469
+ Returns:
470
+ dict: Dictionary with 'total' and 'approved' counts
471
+ """
472
+ checks = self.list()
473
+ return {"total": len(checks), "approved": len([c for c in checks if c.is_checked])}
recce/models/types.py CHANGED
@@ -5,6 +5,8 @@ from typing import Dict, List, Literal, Optional, Set
5
5
 
6
6
  from pydantic import UUID4, BaseModel, Field
7
7
 
8
+ from recce.util.pydantic_model import pydantic_model_dump
9
+
8
10
 
9
11
  class RunType(Enum):
10
12
  SIMPLE = "simple"
@@ -36,8 +38,6 @@ class RunStatus(Enum):
36
38
  FAILED = "failed"
37
39
  CANCELLED = "cancelled"
38
40
  RUNNING = "running"
39
- # This is a special status only in v0.36.0. Replaced by FINISHED. To be removed in the future.
40
- SUCCESSFUL = "successful"
41
41
 
42
42
 
43
43
  class Run(BaseModel):
@@ -52,6 +52,39 @@ class Run(BaseModel):
52
52
  run_id: UUID4 = Field(default_factory=uuid.uuid4)
53
53
  run_at: str = Field(default_factory=lambda: datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"))
54
54
 
55
+ def __init__(self, **data):
56
+ type = data.get("type")
57
+
58
+ if "result" in data and data["result"] is not None:
59
+ result = data.get("result")
60
+
61
+ if type in [RunType.QUERY.value, RunType.QUERY_BASE.value]:
62
+ from recce.tasks.query import QueryResult
63
+
64
+ data["result"] = pydantic_model_dump(QueryResult(**result))
65
+ elif type == RunType.QUERY_DIFF.value:
66
+ from recce.tasks.query import QueryDiffResult
67
+
68
+ data["result"] = pydantic_model_dump(QueryDiffResult(**result))
69
+ elif type == RunType.PROFILE.value:
70
+ from recce.tasks.profile import ProfileResult
71
+
72
+ data["result"] = pydantic_model_dump(ProfileResult(**result))
73
+ elif type == RunType.PROFILE_DIFF.value:
74
+ from recce.tasks.profile import ProfileDiffResult
75
+
76
+ data["result"] = pydantic_model_dump(ProfileDiffResult(**result))
77
+ elif type == RunType.VALUE_DIFF.value:
78
+ from recce.tasks.valuediff import ValueDiffResult
79
+
80
+ data["result"] = pydantic_model_dump(ValueDiffResult(**result))
81
+ elif type == RunType.VALUE_DIFF_DETAIL.value:
82
+ from recce.tasks.valuediff import ValueDiffDetailResult
83
+
84
+ data["result"] = pydantic_model_dump(ValueDiffDetailResult(**result))
85
+
86
+ super().__init__(**data)
87
+
55
88
 
56
89
  class Check(BaseModel):
57
90
  name: str
@@ -60,8 +93,11 @@ class Check(BaseModel):
60
93
  params: Optional[dict] = {}
61
94
  view_options: Optional[dict] = {}
62
95
  check_id: UUID4 = Field(default_factory=uuid.uuid4)
96
+ session_id: Optional[UUID4] = Field(default=None)
63
97
  is_checked: bool = False
64
98
  is_preset: bool = False
99
+ created_by: Optional[str] = None
100
+ updated_by: Optional[str] = None
65
101
  created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc).replace(microsecond=0))
66
102
  updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc).replace(microsecond=0))
67
103
 
@@ -151,6 +187,29 @@ class CllNode(BaseModel):
151
187
  # Column to column dependencies
152
188
  columns: Dict[str, CllColumn] = Field(default_factory=dict)
153
189
 
190
+ # If the node is impacted. Only used if option 'change_analysis' is set
191
+ impacted: Optional[bool] = None
192
+
193
+ @classmethod
194
+ def build_cll_node(cls, manifest, resource_key, node_id) -> Optional["CllNode"]:
195
+ resources = getattr(manifest, resource_key)
196
+ if node_id not in resources:
197
+ return None
198
+ n = resources[node_id]
199
+ if resource_key == "nodes" and n.resource_type not in ["model", "seed", "snapshot"]:
200
+ return None
201
+ cll_node = CllNode(
202
+ id=n.unique_id,
203
+ name=n.name,
204
+ package_name=n.package_name,
205
+ resource_type=n.resource_type,
206
+ )
207
+ if resource_key == "sources":
208
+ cll_node.source_name = n.source_name
209
+ elif resource_key == "nodes":
210
+ cll_node.raw_code = n.raw_code
211
+ return cll_node
212
+
154
213
 
155
214
  class CllData(BaseModel):
156
215
  nodes: Dict[str, CllNode] = Field(default_factory=dict)
recce/pull_request.py CHANGED
@@ -83,7 +83,7 @@ def fetch_pr_metadata_from_event_path() -> Optional[dict]:
83
83
  github_repository = os.getenv("GITHUB_REPOSITORY")
84
84
  if event_path:
85
85
  try:
86
- with open(event_path, "r") as event_file:
86
+ with open(event_path, "r", encoding="utf-8") as event_file:
87
87
  event_data = json.load(event_file)
88
88
 
89
89
  pr_id = event_data["number"]