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
@@ -0,0 +1,190 @@
1
+ """
2
+ Recce Cloud API client for check event operations.
3
+
4
+ This module provides methods for managing check events (timeline/conversation) in Recce Cloud,
5
+ including CRUD operations for comments and retrieving state change events.
6
+ """
7
+
8
+ from typing import Dict, List
9
+
10
+ from recce.util.cloud.base import CloudBase
11
+
12
+
13
+ class CheckEventsCloud(CloudBase):
14
+ """
15
+ Client for Recce Cloud check event operations.
16
+
17
+ Provides methods to list, create, update, and delete check events
18
+ (comments and state changes) within Recce Cloud sessions.
19
+
20
+ Examples:
21
+ >>> client = CheckEventsCloud(token="your-api-token")
22
+ >>> events = client.list_events(
23
+ ... org_id="org123",
24
+ ... project_id="proj456",
25
+ ... session_id="sess789",
26
+ ... check_id="check001"
27
+ ... )
28
+ """
29
+
30
+ def _build_events_url(self, org_id: str, project_id: str, session_id: str, check_id: str) -> str:
31
+ """Build the base URL for check events endpoints."""
32
+ return f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/checks/{check_id}/events"
33
+
34
+ def list_events(self, org_id: str, project_id: str, session_id: str, check_id: str) -> List[Dict]:
35
+ """
36
+ List all events for a check in chronological order.
37
+
38
+ Args:
39
+ org_id: Organization ID
40
+ project_id: Project ID
41
+ session_id: Session ID
42
+ check_id: Check ID
43
+
44
+ Returns:
45
+ List of event dictionaries
46
+
47
+ Raises:
48
+ RecceCloudException: If the request fails
49
+
50
+ Example:
51
+ >>> events = client.list_events("org123", "proj456", "sess789", "check001")
52
+ >>> for event in events:
53
+ ... print(f"{event['event_type']}: {event['content']}")
54
+ """
55
+ api_url = self._build_events_url(org_id, project_id, session_id, check_id)
56
+ query_params = {"include_deleted": True}
57
+ response = self._request("GET", api_url, params=query_params)
58
+
59
+ self._raise_for_status(
60
+ response,
61
+ "Failed to list check events from Recce Cloud.",
62
+ )
63
+
64
+ data = response.json()
65
+ # Response is wrapped: {"events": [...]}
66
+ return data.get("events", [])
67
+
68
+ def get_event(self, org_id: str, project_id: str, session_id: str, check_id: str, event_id: str) -> Dict:
69
+ """
70
+ Get a specific event by ID.
71
+
72
+ Args:
73
+ org_id: Organization ID
74
+ project_id: Project ID
75
+ session_id: Session ID
76
+ check_id: Check ID
77
+ event_id: Event ID
78
+
79
+ Returns:
80
+ Event dictionary
81
+
82
+ Raises:
83
+ RecceCloudException: If the request fails or event not found
84
+ """
85
+ api_url = f"{self._build_events_url(org_id, project_id, session_id, check_id)}/{event_id}"
86
+ response = self._request("GET", api_url)
87
+
88
+ self._raise_for_status(
89
+ response,
90
+ f"Failed to get check event {event_id} from Recce Cloud.",
91
+ )
92
+
93
+ data = response.json()
94
+ # Response is wrapped: {"event": {...}}
95
+ return data.get("event", {})
96
+
97
+ def create_comment(self, org_id: str, project_id: str, session_id: str, check_id: str, content: str) -> Dict:
98
+ """
99
+ Create a new comment on a check.
100
+
101
+ Args:
102
+ org_id: Organization ID
103
+ project_id: Project ID
104
+ session_id: Session ID
105
+ check_id: Check ID
106
+ content: Comment content (plain text or markdown)
107
+
108
+ Returns:
109
+ Created event dictionary
110
+
111
+ Raises:
112
+ RecceCloudException: If the request fails
113
+
114
+ Example:
115
+ >>> event = client.create_comment(
116
+ ... "org123", "proj456", "sess789", "check001",
117
+ ... "This looks good to me!"
118
+ ... )
119
+ >>> print(f"Created comment with ID: {event['id']}")
120
+ """
121
+ api_url = self._build_events_url(org_id, project_id, session_id, check_id)
122
+ response = self._request("POST", api_url, json={"content": content})
123
+
124
+ self._raise_for_status(
125
+ response,
126
+ "Failed to create comment in Recce Cloud.",
127
+ )
128
+
129
+ data = response.json()
130
+ # Response is wrapped: {"event": {...}}
131
+ return data.get("event", {})
132
+
133
+ def update_comment(
134
+ self, org_id: str, project_id: str, session_id: str, check_id: str, event_id: str, content: str
135
+ ) -> Dict:
136
+ """
137
+ Update an existing comment.
138
+
139
+ Only the author or an admin can update a comment.
140
+
141
+ Args:
142
+ org_id: Organization ID
143
+ project_id: Project ID
144
+ session_id: Session ID
145
+ check_id: Check ID
146
+ event_id: Event ID of the comment to update
147
+ content: New comment content
148
+
149
+ Returns:
150
+ Updated event dictionary
151
+
152
+ Raises:
153
+ RecceCloudException: If the request fails or user is not authorized
154
+ """
155
+ api_url = f"{self._build_events_url(org_id, project_id, session_id, check_id)}/{event_id}"
156
+ response = self._request("PATCH", api_url, json={"content": content})
157
+
158
+ self._raise_for_status(
159
+ response,
160
+ f"Failed to update comment {event_id} in Recce Cloud.",
161
+ )
162
+
163
+ data = response.json()
164
+ # Response is wrapped: {"event": {...}}
165
+ return data.get("event", {})
166
+
167
+ def delete_comment(self, org_id: str, project_id: str, session_id: str, check_id: str, event_id: str) -> None:
168
+ """
169
+ Delete a comment (soft delete).
170
+
171
+ Only the author or an admin can delete a comment.
172
+ The comment will be marked as deleted but remain in the timeline.
173
+
174
+ Args:
175
+ org_id: Organization ID
176
+ project_id: Project ID
177
+ session_id: Session ID
178
+ check_id: Check ID
179
+ event_id: Event ID of the comment to delete
180
+
181
+ Raises:
182
+ RecceCloudException: If the request fails or user is not authorized
183
+ """
184
+ api_url = f"{self._build_events_url(org_id, project_id, session_id, check_id)}/{event_id}"
185
+ response = self._request("DELETE", api_url)
186
+
187
+ self._raise_for_status(
188
+ response,
189
+ f"Failed to delete comment {event_id} in Recce Cloud.",
190
+ )
@@ -0,0 +1,242 @@
1
+ """
2
+ Recce Cloud API client for check operations.
3
+
4
+ This module provides methods for managing checks (validation operations) in Recce Cloud,
5
+ including CRUD operations for checks within sessions.
6
+ """
7
+
8
+ from typing import Dict, List
9
+
10
+ from recce.util.cloud.base import CloudBase
11
+
12
+
13
+ class ChecksCloud(CloudBase):
14
+ """
15
+ Client for Recce Cloud check operations.
16
+
17
+ Provides methods to list, create, retrieve, update, and delete checks
18
+ within Recce Cloud sessions.
19
+
20
+ Examples:
21
+ >>> client = ChecksCloud(token="your-api-token")
22
+ >>> checks = client.list_checks(org_id="org123", project_id="proj456", session_id="sess789")
23
+ >>> check = client.get_check(org_id="org123", project_id="proj456",
24
+ ... session_id="sess789", check_id="check001")
25
+ """
26
+
27
+ def list_checks(self, org_id: str, project_id: str, session_id: str) -> List[Dict]:
28
+ """
29
+ List all checks in a session.
30
+
31
+ Args:
32
+ org_id: Organization ID
33
+ project_id: Project ID
34
+ session_id: Session ID
35
+
36
+ Returns:
37
+ List of check dictionaries
38
+
39
+ Raises:
40
+ RecceCloudException: If the request fails
41
+
42
+ Example:
43
+ >>> checks = client.list_checks("org123", "proj456", "sess789")
44
+ >>> print(f"Found {len(checks)} checks")
45
+ """
46
+ api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/checks"
47
+ response = self._request("GET", api_url)
48
+
49
+ self._raise_for_status(
50
+ response,
51
+ "Failed to list checks from Recce Cloud.",
52
+ )
53
+
54
+ data = response.json()
55
+ return data.get("checks", [])
56
+
57
+ def create_check(
58
+ self,
59
+ org_id: str,
60
+ project_id: str,
61
+ session_id: str,
62
+ check_data: Dict,
63
+ ) -> Dict:
64
+ """
65
+ Create a new check in a session.
66
+
67
+ Args:
68
+ org_id: Organization ID
69
+ project_id: Project ID
70
+ session_id: Session ID
71
+ check_data: Check data to create (should include check type, params, etc.)
72
+
73
+ Returns:
74
+ Created check dictionary
75
+
76
+ Raises:
77
+ RecceCloudException: If the request fails
78
+
79
+ Example:
80
+ >>> check_data = {
81
+ ... "name": "Schema Check",
82
+ ... "type": "schema_diff",
83
+ ... "params": {"model": "customers"}
84
+ ... }
85
+ >>> check = client.create_check("org123", "proj456", "sess789", check_data)
86
+ >>> print(f"Created check with ID: {check['id']}")
87
+ """
88
+ api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/checks"
89
+ response = self._request("POST", api_url, json=check_data)
90
+
91
+ self._raise_for_status(
92
+ response,
93
+ "Failed to create check in Recce Cloud.",
94
+ )
95
+ data = response.json()
96
+ return data.get("check")
97
+
98
+ def get_check(
99
+ self,
100
+ org_id: str,
101
+ project_id: str,
102
+ session_id: str,
103
+ check_id: str,
104
+ ) -> Dict:
105
+ """
106
+ Get a specific check by ID.
107
+
108
+ Args:
109
+ org_id: Organization ID
110
+ project_id: Project ID
111
+ session_id: Session ID
112
+ check_id: Check ID
113
+
114
+ Returns:
115
+ Check dictionary
116
+
117
+ Raises:
118
+ RecceCloudException: If the request fails or check not found
119
+
120
+ Example:
121
+ >>> check = client.get_check("org123", "proj456", "sess789", "check001")
122
+ >>> print(f"Check name: {check['name']}")
123
+ >>> print(f"Check status: {check['status']}")
124
+ """
125
+ api_url = (
126
+ f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/checks/{check_id}"
127
+ )
128
+ response = self._request("GET", api_url)
129
+
130
+ self._raise_for_status(
131
+ response,
132
+ f"Failed to get check {check_id} from Recce Cloud.",
133
+ )
134
+
135
+ data = response.json()
136
+ return data.get("check", {})
137
+
138
+ def update_check(
139
+ self,
140
+ org_id: str,
141
+ project_id: str,
142
+ session_id: str,
143
+ check_id: str,
144
+ check_data: Dict,
145
+ ) -> Dict:
146
+ """
147
+ Update an existing check.
148
+
149
+ Args:
150
+ org_id: Organization ID
151
+ project_id: Project ID
152
+ session_id: Session ID
153
+ check_id: Check ID
154
+ check_data: Updated check data (partial updates supported)
155
+
156
+ Returns:
157
+ Updated check dictionary
158
+
159
+ Raises:
160
+ RecceCloudException: If the request fails
161
+
162
+ Example:
163
+ >>> update_data = {
164
+ ... "status": "approved",
165
+ ... "notes": "Validated successfully"
166
+ ... }
167
+ >>> check = client.update_check("org123", "proj456", "sess789", "check001", update_data)
168
+ >>> print(f"Updated check status: {check['status']}")
169
+ """
170
+ api_url = (
171
+ f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/checks/{check_id}"
172
+ )
173
+ response = self._request("PATCH", api_url, json=check_data)
174
+
175
+ self._raise_for_status(
176
+ response,
177
+ f"Failed to update check {check_id} in Recce Cloud.",
178
+ )
179
+
180
+ data = response.json()
181
+ return data.get("check", {})
182
+
183
+ def delete_check(
184
+ self,
185
+ org_id: str,
186
+ project_id: str,
187
+ session_id: str,
188
+ check_id: str,
189
+ ) -> None:
190
+ """
191
+ Delete a check.
192
+
193
+ Args:
194
+ org_id: Organization ID
195
+ project_id: Project ID
196
+ session_id: Session ID
197
+ check_id: Check ID
198
+
199
+ Raises:
200
+ RecceCloudException: If the request fails
201
+
202
+ Example:
203
+ >>> client.delete_check("org123", "proj456", "sess789", "check001")
204
+ >>> print("Check deleted successfully")
205
+ """
206
+ api_url = (
207
+ f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/checks/{check_id}"
208
+ )
209
+ response = self._request("DELETE", api_url)
210
+
211
+ # DELETE typically returns 204 No Content on success
212
+ if response.status_code not in (200, 204):
213
+ self._raise_for_status(
214
+ response,
215
+ f"Failed to delete check {check_id} from Recce Cloud.",
216
+ )
217
+
218
+ def create_preset_check(self, org_id: str, project_id: str, check_data: Dict):
219
+ """
220
+ Create a preset check from an existing check.
221
+
222
+ Args:
223
+ org_id: Organization ID
224
+ project_id: Project ID
225
+ check_data: Check data including name, description, type, params, view_options, and order_index
226
+
227
+ Returns:
228
+ Created preset check dictionary
229
+
230
+ Raises:
231
+ RecceCloudException: If the request fails
232
+ """
233
+ api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/preset-checks"
234
+ response = self._request("POST", api_url, json=check_data)
235
+
236
+ self._raise_for_status(
237
+ response,
238
+ "Failed to create preset check in Recce Cloud.",
239
+ )
240
+
241
+ data = response.json()
242
+ return data.get("presetCheck", {})
recce/util/io.py CHANGED
@@ -1,14 +1,14 @@
1
1
  import gzip
2
2
  import os
3
3
  import tempfile
4
- from abc import ABCMeta, abstractmethod, ABC
4
+ from abc import ABC, ABCMeta, abstractmethod
5
5
  from enum import Enum
6
6
 
7
7
 
8
8
  class SupportedFileTypes(Enum):
9
- FILE = 'file'
10
- GZIP = 'gzip'
11
- ZIP = 'zip'
9
+ FILE = "file"
10
+ GZIP = "gzip"
11
+ ZIP = "zip"
12
12
 
13
13
 
14
14
  def file_io_factory(file_type: SupportedFileTypes):
@@ -19,7 +19,7 @@ def file_io_factory(file_type: SupportedFileTypes):
19
19
  elif file_type == SupportedFileTypes.ZIP:
20
20
  return ZipFileIO
21
21
  else:
22
- raise ValueError(f'Unsupported file type: {file_type}')
22
+ raise ValueError(f"Unsupported file type: {file_type}")
23
23
 
24
24
 
25
25
  class AbstractFileIO(metaclass=ABCMeta):
@@ -37,24 +37,24 @@ class AbstractFileIO(metaclass=ABCMeta):
37
37
  class FileIO(AbstractFileIO, ABC):
38
38
  @staticmethod
39
39
  def write(path: str, data: str, **kwargs):
40
- with open(path, 'w') as f:
40
+ with open(path, "w", encoding="utf-8") as f:
41
41
  f.write(data)
42
42
 
43
43
  @staticmethod
44
44
  def read(path: str, **kwargs) -> str:
45
- with open(path, 'r') as f:
45
+ with open(path, "r", encoding="utf-8") as f:
46
46
  return f.read()
47
47
 
48
48
 
49
49
  class GzipFileIO(AbstractFileIO, ABC):
50
50
  @staticmethod
51
51
  def write(path: str, data: str, **kwargs):
52
- with gzip.open(path, 'wt') as f:
52
+ with gzip.open(path, "wt") as f:
53
53
  f.write(data)
54
54
 
55
55
  @staticmethod
56
56
  def read(path: str, **kwargs) -> str:
57
- with gzip.open(path, 'rt') as f:
57
+ with gzip.open(path, "rt") as f:
58
58
  return f.read()
59
59
 
60
60
  @staticmethod
@@ -68,18 +68,22 @@ class ZipFileIO(AbstractFileIO, ABC):
68
68
  def _is_pyminizip_installed():
69
69
  try:
70
70
  import pyminizip
71
+
72
+ # Use the module to avoid F401
73
+ return pyminizip is not None
71
74
  except ImportError:
72
- raise ImportError('pyminizip is not installed. Please install it using `pip install pyminizip`')
75
+ raise ImportError("pyminizip is not installed. Please install it using `pip install pyminizip`")
73
76
 
74
77
  @staticmethod
75
78
  def read(path: str, **kwargs) -> str:
76
79
  ZipFileIO._is_pyminizip_installed()
77
80
  import pyminizip
81
+
78
82
  cwd = os.getcwd()
79
- password = kwargs.get('password')
80
- zip_dir_name = kwargs.get('zip_dir_name')
83
+ password = kwargs.get("password")
84
+ zip_dir_name = kwargs.get("zip_dir_name")
81
85
  if zip_dir_name is None:
82
- raise ValueError('zip_dir_name is required for zipping')
86
+ raise ValueError("zip_dir_name is required for zipping")
83
87
 
84
88
  try:
85
89
  with tempfile.TemporaryDirectory() as tmp_dir:
@@ -88,7 +92,7 @@ class ZipFileIO(AbstractFileIO, ABC):
88
92
  content = FileIO.read(tmp_file)
89
93
  except Exception as e:
90
94
  error_msg = str(e)
91
- if '-3' in error_msg:
95
+ if "-3" in error_msg:
92
96
  raise Exception("Invalid password to uncompress state file.")
93
97
  raise Exception(f"Failed to uncompress state file: {error_msg}")
94
98
  finally:
@@ -100,11 +104,12 @@ class ZipFileIO(AbstractFileIO, ABC):
100
104
  def write(path: str, data: str, **kwargs):
101
105
  ZipFileIO._is_pyminizip_installed()
102
106
  import pyminizip
107
+
103
108
  cwd = os.getcwd()
104
- password = kwargs.get('password')
105
- zip_dir_name = kwargs.get('zip_dir_name')
109
+ password = kwargs.get("password")
110
+ zip_dir_name = kwargs.get("zip_dir_name")
106
111
  if zip_dir_name is None:
107
- raise ValueError('zip_dir_name is required for zipping')
112
+ raise ValueError("zip_dir_name is required for zipping")
108
113
 
109
114
  try:
110
115
  with tempfile.TemporaryDirectory() as tmp_dir:
recce/util/lineage.py CHANGED
@@ -1,34 +1,83 @@
1
- def find_upstream(node, parent_map):
1
+ from typing import Dict, Iterable, Set, Tuple
2
+
3
+ from recce.models.types import CllColumn, CllNode
4
+
5
+
6
+ def find_upstream(node_ids: Iterable, parent_map):
2
7
  visited = set()
3
8
  upstream = set()
4
9
 
5
- def dfs(current):
10
+ stack = list(node_ids)
11
+ while stack:
12
+ current = stack.pop()
6
13
  if current in visited:
7
- return
14
+ continue
8
15
  visited.add(current)
9
-
10
16
  parents = parent_map.get(current, [])
11
17
  for parent in parents:
12
- upstream.add(parent)
13
- dfs(parent)
14
-
15
- dfs(node)
18
+ if parent not in upstream:
19
+ upstream.add(parent)
20
+ stack.append(parent)
16
21
  return upstream
17
22
 
18
23
 
19
- def find_downstream(node, child_map):
24
+ def find_downstream(node_ids: Iterable, child_map):
20
25
  visited = set()
21
26
  downstream = set()
22
27
 
23
- def dfs(current):
28
+ stack = list(node_ids)
29
+ while stack:
30
+ current = stack.pop()
24
31
  if current in visited:
25
- return
32
+ continue
26
33
  visited.add(current)
27
-
28
34
  children = child_map.get(current, [])
29
35
  for child in children:
30
- downstream.add(child)
31
- dfs(child)
32
-
33
- dfs(node)
36
+ if child not in downstream:
37
+ downstream.add(child)
38
+ stack.append(child)
34
39
  return downstream
40
+
41
+
42
+ def find_column_dependencies(node_column_id: str, parent_map: Dict, child_map: Dict) -> Tuple[Set, Set]:
43
+ upstream_cols = find_upstream([node_column_id], parent_map)
44
+ downstream_cols = find_downstream([node_column_id], child_map)
45
+ return upstream_cols, downstream_cols
46
+
47
+
48
+ def filter_lineage_vertices(
49
+ lineage_nodes: Dict[str, CllNode], lineage_columns: Dict[str, CllColumn], relevant_columns: Set[str]
50
+ ) -> Tuple[Dict[str, CllNode], Dict[str, CllColumn]]:
51
+ nodes = {}
52
+ columns = {}
53
+
54
+ for node_id, node in lineage_nodes.items():
55
+ if node_id in relevant_columns:
56
+ nodes[node_id] = node
57
+
58
+ for col_id, column in lineage_columns.items():
59
+ if col_id in relevant_columns:
60
+ columns[col_id] = column
61
+
62
+ return nodes, columns
63
+
64
+
65
+ def filter_dependency_maps(
66
+ parent_map: Dict, child_map: Dict, relevant_ids: Set
67
+ ) -> Tuple[Dict[str, Set], Dict[str, Set]]:
68
+ p_map = {}
69
+ c_map = {}
70
+ for node_id, parents in parent_map.items():
71
+ if node_id in relevant_ids:
72
+ p_map[node_id] = {p for p in parents if p in relevant_ids}
73
+
74
+ for node_id, children in child_map.items():
75
+ if node_id in relevant_ids:
76
+ c_map[node_id] = {c for c in children if c in relevant_ids}
77
+
78
+ return p_map, c_map
79
+
80
+
81
+ def build_column_key(node_id: str, column_name: str) -> str:
82
+ """Build a unique column key from node name and column name."""
83
+ return f"{node_id}_{column_name}"
recce/util/logger.py CHANGED
@@ -16,7 +16,7 @@ class CustomFormatter(logging.Formatter):
16
16
  logging.INFO: green + format + reset,
17
17
  logging.WARNING: yellow + format + reset,
18
18
  logging.ERROR: red + format + reset,
19
- logging.CRITICAL: bold_red + format + reset
19
+ logging.CRITICAL: bold_red + format + reset,
20
20
  }
21
21
 
22
22
  def format(self, record):
@@ -0,0 +1,45 @@
1
+ from enum import Enum
2
+ from typing import Union
3
+
4
+ from recce.util.recce_cloud import (
5
+ get_recce_cloud_onboarding_state,
6
+ set_recce_cloud_onboarding_state,
7
+ )
8
+
9
+
10
+ class OnboardingState(Enum):
11
+ SIGNUP_SUCCESSFUL = "signup_successful"
12
+ LAUNCHED_WITH_TOKEN = "launched_with_token"
13
+ CONFIGURE_TWO_ENV = "configure_two_env"
14
+
15
+
16
+ def update_onboarding_state(api_token: Union[str, bool, None], is_single_env: bool) -> OnboardingState:
17
+ if api_token:
18
+ # existing onboarding_state values -> new, launched, launched_with_two_envs
19
+ # new -> launched
20
+ # new -> launched_with_two_envs
21
+ # launched -> launched_with_two_envs
22
+ cloud_onboarding_state = get_recce_cloud_onboarding_state(api_token)
23
+
24
+ if cloud_onboarding_state == OnboardingState.SIGNUP_SUCCESSFUL.value:
25
+ # User has an API Token and is a "new" user
26
+ if is_single_env:
27
+ # Mark the onboarding state as "launched" if the user is new
28
+ set_recce_cloud_onboarding_state(api_token, OnboardingState.LAUNCHED_WITH_TOKEN.value)
29
+ return OnboardingState.LAUNCHED_WITH_TOKEN
30
+ else:
31
+ set_recce_cloud_onboarding_state(api_token, OnboardingState.CONFIGURE_TWO_ENV.value)
32
+ return OnboardingState.CONFIGURE_TWO_ENV
33
+ elif cloud_onboarding_state == OnboardingState.LAUNCHED_WITH_TOKEN.value:
34
+ # User has an API Token and has Two Environments
35
+ if is_single_env:
36
+ # Just return the current state
37
+ return OnboardingState.LAUNCHED_WITH_TOKEN
38
+ else:
39
+ set_recce_cloud_onboarding_state(api_token, OnboardingState.CONFIGURE_TWO_ENV.value)
40
+ return OnboardingState.CONFIGURE_TWO_ENV
41
+ elif cloud_onboarding_state == OnboardingState.CONFIGURE_TWO_ENV.value:
42
+ # Just return the current state
43
+ return OnboardingState.CONFIGURE_TWO_ENV
44
+
45
+ return OnboardingState.SIGNUP_SUCCESSFUL