recce-nightly 1.2.0.20250506__py3-none-any.whl → 1.26.0.20251124__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of recce-nightly might be problematic. Click here for more details.

Files changed (213) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +27 -22
  3. recce/adapter/base.py +11 -14
  4. recce/adapter/dbt_adapter/__init__.py +810 -480
  5. recce/adapter/dbt_adapter/dbt_version.py +3 -0
  6. recce/adapter/sqlmesh_adapter.py +24 -35
  7. recce/apis/check_api.py +39 -28
  8. recce/apis/check_func.py +33 -27
  9. recce/apis/run_api.py +25 -19
  10. recce/apis/run_func.py +29 -23
  11. recce/artifact.py +119 -51
  12. recce/cli.py +1299 -323
  13. recce/config.py +42 -33
  14. recce/connect_to_cloud.py +138 -0
  15. recce/core.py +55 -47
  16. recce/data/404.html +1 -1
  17. recce/data/__next.__PAGE__.txt +10 -0
  18. recce/data/__next._full.txt +23 -0
  19. recce/data/__next._head.txt +8 -0
  20. recce/data/__next._index.txt +8 -0
  21. recce/data/__next._tree.txt +5 -0
  22. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
  23. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
  24. recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
  25. recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
  26. recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
  27. recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
  28. recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
  29. recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
  30. recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
  31. recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
  32. recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
  33. recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
  34. recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
  35. recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
  36. recce/data/_next/static/chunks/99d638224186c118.js +1 -0
  37. recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
  38. recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
  39. recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
  40. recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
  41. recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
  42. recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
  43. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  44. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  45. recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
  46. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  47. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
  48. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  49. recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
  50. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  51. recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
  52. recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
  53. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  54. recce/data/_next/static/media/reload-image.7aa931c7.svg +4 -0
  55. recce/data/_not-found/__next._full.txt +17 -0
  56. recce/data/_not-found/__next._head.txt +8 -0
  57. recce/data/_not-found/__next._index.txt +8 -0
  58. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  59. recce/data/_not-found/__next._not-found.txt +4 -0
  60. recce/data/_not-found/__next._tree.txt +3 -0
  61. recce/data/_not-found.html +1 -0
  62. recce/data/_not-found.txt +17 -0
  63. recce/data/auth_callback.html +68 -0
  64. recce/data/imgs/reload-image.svg +4 -0
  65. recce/data/index.html +1 -27
  66. recce/data/index.txt +23 -7
  67. recce/diff.py +6 -12
  68. recce/event/__init__.py +86 -74
  69. recce/event/collector.py +33 -22
  70. recce/event/track.py +49 -27
  71. recce/exceptions.py +1 -1
  72. recce/git.py +7 -7
  73. recce/github.py +57 -53
  74. recce/mcp_server.py +716 -0
  75. recce/models/__init__.py +4 -1
  76. recce/models/check.py +6 -7
  77. recce/models/run.py +1 -0
  78. recce/models/types.py +131 -28
  79. recce/pull_request.py +27 -25
  80. recce/run.py +165 -121
  81. recce/server.py +303 -111
  82. recce/state/__init__.py +31 -0
  83. recce/state/cloud.py +632 -0
  84. recce/state/const.py +26 -0
  85. recce/state/local.py +56 -0
  86. recce/state/state.py +119 -0
  87. recce/state/state_loader.py +174 -0
  88. recce/summary.py +188 -143
  89. recce/tasks/__init__.py +19 -3
  90. recce/tasks/core.py +11 -13
  91. recce/tasks/dataframe.py +82 -18
  92. recce/tasks/histogram.py +69 -34
  93. recce/tasks/lineage.py +2 -2
  94. recce/tasks/profile.py +152 -86
  95. recce/tasks/query.py +139 -87
  96. recce/tasks/rowcount.py +37 -31
  97. recce/tasks/schema.py +18 -15
  98. recce/tasks/top_k.py +35 -35
  99. recce/tasks/valuediff.py +216 -152
  100. recce/util/__init__.py +3 -0
  101. recce/util/api_token.py +80 -0
  102. recce/util/breaking.py +87 -85
  103. recce/util/cll.py +274 -219
  104. recce/util/io.py +22 -17
  105. recce/util/lineage.py +65 -16
  106. recce/util/logger.py +1 -1
  107. recce/util/onboarding_state.py +45 -0
  108. recce/util/perf_tracking.py +85 -0
  109. recce/util/recce_cloud.py +322 -72
  110. recce/util/singleton.py +4 -4
  111. recce/yaml/__init__.py +7 -10
  112. recce_cloud/__init__.py +24 -0
  113. recce_cloud/api/__init__.py +17 -0
  114. recce_cloud/api/base.py +111 -0
  115. recce_cloud/api/client.py +150 -0
  116. recce_cloud/api/exceptions.py +26 -0
  117. recce_cloud/api/factory.py +63 -0
  118. recce_cloud/api/github.py +76 -0
  119. recce_cloud/api/gitlab.py +82 -0
  120. recce_cloud/artifact.py +57 -0
  121. recce_cloud/ci_providers/__init__.py +9 -0
  122. recce_cloud/ci_providers/base.py +82 -0
  123. recce_cloud/ci_providers/detector.py +147 -0
  124. recce_cloud/ci_providers/github_actions.py +136 -0
  125. recce_cloud/ci_providers/gitlab_ci.py +130 -0
  126. recce_cloud/cli.py +245 -0
  127. recce_cloud/upload.py +214 -0
  128. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
  129. recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
  130. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
  131. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
  132. tests/adapter/dbt_adapter/conftest.py +9 -5
  133. tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
  134. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
  135. tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
  136. tests/adapter/dbt_adapter/test_selector.py +22 -21
  137. tests/recce_cloud/__init__.py +0 -0
  138. tests/recce_cloud/test_ci_providers.py +351 -0
  139. tests/recce_cloud/test_cli.py +372 -0
  140. tests/recce_cloud/test_client.py +273 -0
  141. tests/recce_cloud/test_platform_clients.py +333 -0
  142. tests/tasks/conftest.py +1 -1
  143. tests/tasks/test_histogram.py +58 -66
  144. tests/tasks/test_lineage.py +36 -23
  145. tests/tasks/test_preset_checks.py +45 -31
  146. tests/tasks/test_profile.py +339 -15
  147. tests/tasks/test_query.py +46 -46
  148. tests/tasks/test_row_count.py +65 -46
  149. tests/tasks/test_schema.py +65 -42
  150. tests/tasks/test_top_k.py +22 -18
  151. tests/tasks/test_valuediff.py +43 -32
  152. tests/test_cli.py +174 -60
  153. tests/test_cli_mcp_optional.py +45 -0
  154. tests/test_cloud_listing_cli.py +324 -0
  155. tests/test_config.py +7 -9
  156. tests/test_connect_to_cloud.py +82 -0
  157. tests/test_core.py +151 -4
  158. tests/test_dbt.py +7 -7
  159. tests/test_mcp_server.py +332 -0
  160. tests/test_pull_request.py +1 -1
  161. tests/test_server.py +25 -19
  162. tests/test_summary.py +29 -17
  163. recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
  164. recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
  165. recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
  166. recce/data/_next/static/chunks/368-7587b306577df275.js +0 -65
  167. recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
  168. recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
  169. recce/data/_next/static/chunks/3a92ee20-3b5d922d4157af5e.js +0 -1
  170. recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
  171. recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
  172. recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
  173. recce/data/_next/static/chunks/6ef81909-694dc38134099299.js +0 -1
  174. recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
  175. recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
  176. recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
  177. recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
  178. recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
  179. recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
  180. recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
  181. recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
  182. recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
  183. recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
  184. recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
  185. recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
  186. recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
  187. recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
  188. recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
  189. recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
  190. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  191. recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
  192. recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
  193. recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
  194. recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
  195. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  196. recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
  197. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  198. recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
  199. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
  200. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  201. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  202. recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
  203. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  204. recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
  205. recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
  206. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  207. recce/state.py +0 -753
  208. recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
  209. tests/test_state.py +0 -123
  210. /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
  211. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  212. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
  213. {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
recce/tasks/query.py CHANGED
@@ -1,14 +1,14 @@
1
1
  import typing
2
- from typing import Optional, Tuple, List
2
+ from typing import List, Optional, Tuple
3
3
 
4
4
  from pydantic import BaseModel
5
5
 
6
- from .core import Task, TaskResultDiffer, CheckValidator
7
- from .dataframe import DataFrame
8
- from .valuediff import ValueDiffMixin
9
6
  from ..core import default_context
10
7
  from ..exceptions import RecceException
11
8
  from ..models import Check
9
+ from .core import CheckValidator, Task, TaskResultDiffer
10
+ from .dataframe import DataFrame
11
+ from .valuediff import ValueDiffMixin
12
12
 
13
13
  QUERY_LIMIT = 2000
14
14
 
@@ -19,11 +19,8 @@ if typing.TYPE_CHECKING:
19
19
  class QueryMixin:
20
20
  @classmethod
21
21
  def execute_sql_with_limit(
22
- cls,
23
- sql_template,
24
- base: bool = False,
25
- limit: Optional[int] = None
26
- ) -> Tuple['agate.Table', bool]:
22
+ cls, sql_template, base: bool = False, limit: Optional[int] = None
23
+ ) -> Tuple["agate.Table", bool]:
27
24
  """
28
25
  Execute a SQL template and return the result as an agate table.
29
26
  :param sql_template: SQL template to execute
@@ -32,8 +29,10 @@ class QueryMixin:
32
29
  :return: Tuple of agate table and whether there are more rows to fetch
33
30
  """
34
31
  from jinja2.exceptions import TemplateSyntaxError
32
+
35
33
  dbt_adapter = default_context().adapter
36
34
  from dbt.exceptions import TargetNotFoundError
35
+
37
36
  try:
38
37
  sql = dbt_adapter.generate_sql(sql_template, base)
39
38
 
@@ -51,7 +50,7 @@ class QueryMixin:
51
50
  raise RecceException(f"Jinja template error: line {e.lineno}: {str(e)}")
52
51
 
53
52
  @classmethod
54
- def execute_sql(cls, sql_template, base: bool = False) -> 'agate.Table':
53
+ def execute_sql(cls, sql_template, base: bool = False) -> "agate.Table":
55
54
  result, _ = cls.execute_sql_with_limit(sql_template, base)
56
55
  return result
57
56
 
@@ -87,6 +86,7 @@ class QueryTask(Task, QueryMixin):
87
86
 
88
87
  def execute_dbt(self):
89
88
  from recce.adapter.dbt_adapter import DbtAdapter
89
+
90
90
  dbt_adapter: DbtAdapter = default_context().adapter
91
91
 
92
92
  limit = QUERY_LIMIT
@@ -101,9 +101,10 @@ class QueryTask(Task, QueryMixin):
101
101
 
102
102
  def execute_sqlmesh(self):
103
103
  from ..adapter.sqlmesh_adapter import SqlmeshAdapter
104
+
104
105
  sqlmesh_adapter: SqlmeshAdapter = default_context().adapter
105
106
 
106
- sql = self.params.get('sql_template')
107
+ sql = self.params.get("sql_template")
107
108
  limit = QUERY_LIMIT
108
109
  df, more = sqlmesh_adapter.fetchdf_with_limit(sql, base=self.is_base, limit=limit)
109
110
  return DataFrame.from_pandas(df, limit=limit, more=more)
@@ -111,7 +112,7 @@ class QueryTask(Task, QueryMixin):
111
112
  def execute(self):
112
113
  context = default_context()
113
114
 
114
- if context.adapter_type == 'sqlmesh':
115
+ if context.adapter_type == "sqlmesh":
115
116
  return self.execute_sqlmesh()
116
117
  else:
117
118
  return self.execute_dbt()
@@ -139,8 +140,13 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
139
140
  self.connection = None
140
141
  self.legacy_surrogate_key = True
141
142
 
142
- def _query_diff(self, dbt_adapter, sql_template: str, base_sql_template: Optional[str] = None,
143
- preview_change: bool = False):
143
+ def _query_diff(
144
+ self,
145
+ dbt_adapter,
146
+ sql_template: str,
147
+ base_sql_template: Optional[str] = None,
148
+ preview_change: bool = False,
149
+ ):
144
150
  limit = QUERY_LIMIT
145
151
 
146
152
  self.connection = dbt_adapter.get_thread_connection()
@@ -155,40 +161,76 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
155
161
 
156
162
  return QueryDiffResult(
157
163
  base=DataFrame.from_agate(base, limit=limit, more=base_more),
158
- current=DataFrame.from_agate(current, limit=limit, more=current_more)
164
+ current=DataFrame.from_agate(current, limit=limit, more=current_more),
159
165
  )
160
166
 
161
- def _query_diff_join(self, dbt_adapter, sql_template: str, primary_keys: List[str],
162
- base_sql_template: Optional[str] = None, preview_change: bool = False):
167
+ def _query_diff_join(
168
+ self,
169
+ dbt_adapter,
170
+ sql_template: str,
171
+ primary_keys: List[str],
172
+ base_sql_template: Optional[str] = None,
173
+ preview_change: bool = False,
174
+ ):
163
175
 
164
176
  query_template = r"""
165
- {% set a_query %}
166
- {{ base_query }}
167
- {% endset %}
168
-
169
- {% set b_query %}
170
- {{ current_query }}
171
- {% endset %}
172
-
173
- {{ audit_helper.compare_queries(
174
- a_query=a_query,
175
- b_query=b_query,
176
- primary_key=__PRIMARY_KEY__,
177
- summarize=False,
178
- ) }} limit {{ limit }}
179
- """
180
-
181
- if len(primary_keys) > 1:
182
- self._verify_dbt_packages_deps(dbt_adapter)
183
- self.check_cancel()
177
+ with a_query as (
178
+ {{ base_query }}
179
+ ),
180
+
181
+ b_query as (
182
+ {{ current_query }}
183
+ ),
184
+
185
+ a_intersect_b as (
186
+ select * from a_query
187
+ {{ dbt.intersect() }}
188
+ select * from b_query
189
+ ),
190
+
191
+ a_except_b as (
192
+ select * from a_query
193
+ {{ dbt.except() }}
194
+ select * from b_query
195
+ ),
196
+
197
+ b_except_a as (
198
+ select * from b_query
199
+ {{ dbt.except() }}
200
+ select * from a_query
201
+ ),
202
+
203
+ all_records as (
204
+ select
205
+ *,
206
+ true as in_a,
207
+ true as in_b
208
+ from a_intersect_b
209
+
210
+ union all
211
+
212
+ select
213
+ *,
214
+ true as in_a,
215
+ false as in_b
216
+ from a_except_b
217
+
218
+ union all
219
+
220
+ select
221
+ *,
222
+ false as in_a,
223
+ true as in_b
224
+ from b_except_a
225
+ )
184
226
 
185
- if self.legacy_surrogate_key:
186
- new_primary_key = 'dbt_utils.surrogate_key(primary_key)'
187
- else:
188
- new_primary_key = 'dbt_utils.generate_surrogate_key(primary_key)'
189
- else:
190
- new_primary_key = 'primary_key'
191
- query_template = query_template.replace('__PRIMARY_KEY__', new_primary_key)
227
+ select * from all_records
228
+ where not (in_a and in_b)
229
+ order by {{ primary_keys | join(',\n') }}, in_a desc, in_b desc
230
+ limit {{ limit }}
231
+ """
232
+
233
+ self.check_cancel()
192
234
 
193
235
  if preview_change:
194
236
  base_query = dbt_adapter.generate_sql(base_sql_template, base=False)
@@ -196,19 +238,20 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
196
238
  base_query = dbt_adapter.generate_sql(base_sql_template or sql_template, base=True)
197
239
  current_query = dbt_adapter.generate_sql(sql_template, base=False)
198
240
 
199
- sql = dbt_adapter.generate_sql(query_template, context=dict(
200
- base_query=base_query,
201
- current_query=current_query,
202
- primary_key=primary_keys if len(primary_keys) != 1 else primary_keys[0],
203
- limit=QUERY_LIMIT,
204
- ))
241
+ sql = dbt_adapter.generate_sql(
242
+ query_template,
243
+ context=dict(
244
+ base_query=base_query,
245
+ current_query=current_query,
246
+ primary_keys=primary_keys,
247
+ limit=QUERY_LIMIT,
248
+ ),
249
+ )
205
250
 
206
251
  _, table = dbt_adapter.execute(sql, fetch=True)
207
252
  self.check_cancel()
208
253
 
209
- return QueryDiffResult(
210
- diff=DataFrame.from_agate(table)
211
- )
254
+ return QueryDiffResult(diff=DataFrame.from_agate(table))
212
255
 
213
256
  @staticmethod
214
257
  def _select_single_model(model_name):
@@ -216,6 +259,7 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
216
259
 
217
260
  def execute_dbt(self):
218
261
  from recce.adapter.dbt_adapter import DbtAdapter
262
+
219
263
  dbt_adapter: DbtAdapter = default_context().adapter
220
264
 
221
265
  with dbt_adapter.connection_named("query"):
@@ -228,11 +272,20 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
228
272
  preview_change = True
229
273
 
230
274
  if primary_keys:
231
- return self._query_diff_join(dbt_adapter, sql_template, primary_keys,
232
- base_sql_template=base_sql_template, preview_change=preview_change)
233
-
234
- return self._query_diff(dbt_adapter, sql_template, base_sql_template=base_sql_template,
235
- preview_change=preview_change)
275
+ return self._query_diff_join(
276
+ dbt_adapter,
277
+ sql_template,
278
+ primary_keys,
279
+ base_sql_template=base_sql_template,
280
+ preview_change=preview_change,
281
+ )
282
+
283
+ return self._query_diff(
284
+ dbt_adapter,
285
+ sql_template,
286
+ base_sql_template=base_sql_template,
287
+ preview_change=preview_change,
288
+ )
236
289
 
237
290
  def _sqlmesh_query_diff(self, sql, base_sql=None):
238
291
  from ..adapter.sqlmesh_adapter import SqlmeshAdapter
@@ -244,7 +297,7 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
244
297
  curr, curr_more = sqlmesh_adapter.fetchdf_with_limit(sql, base=False, limit=limit)
245
298
  return QueryDiffResult(
246
299
  base=DataFrame.from_pandas(base, limit=limit, more=base_more),
247
- current=DataFrame.from_pandas(curr, limit=limit, more=curr_more)
300
+ current=DataFrame.from_pandas(curr, limit=limit, more=curr_more),
248
301
  )
249
302
 
250
303
  def _sqlmesh_query_diff_join(self, sql, primary_keys, base_sql=None):
@@ -257,21 +310,18 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
257
310
  expr_curr = sqlmesh_adapter.replace_virtual_tables(sql, base=False)
258
311
  import sqlglot as g
259
312
 
260
- expr = g.select(
261
- '*',
262
- ).with_(
263
- 'a', as_=expr_base
264
- ).with_(
265
- 'b', as_=expr_curr
266
- ).with_(
267
- 'a_interset_b', as_='select * from a intersect select * from b'
268
- ).with_(
269
- 'a_except_b', as_='select * from a except select * from b'
270
- ).with_(
271
- 'b_except_a', as_='select * from b except select * from a'
272
- ).with_(
273
- 'all_records',
274
- as_='''
313
+ expr = (
314
+ g.select(
315
+ "*",
316
+ )
317
+ .with_("a", as_=expr_base)
318
+ .with_("b", as_=expr_curr)
319
+ .with_("a_interset_b", as_="select * from a intersect select * from b")
320
+ .with_("a_except_b", as_="select * from a except select * from b")
321
+ .with_("b_except_a", as_="select * from b except select * from a")
322
+ .with_(
323
+ "all_records",
324
+ as_="""
275
325
  SELECT
276
326
  *,
277
327
  TRUE AS in_a,
@@ -289,19 +339,21 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
289
339
  FALSE AS in_a,
290
340
  TRUE AS in_b
291
341
  FROM b_except_a
292
- '''
293
- ).with_(
294
- 'final',
295
- as_=f'''
342
+ """,
343
+ )
344
+ .with_(
345
+ "final",
346
+ as_=f"""
296
347
  select * from all_records
297
348
  where not (in_a and in_b)
298
349
  order by {", ".join(primary_keys)}, in_a desc, in_b desc
299
- '''
300
- ).from_('final').limit(1000)
301
- diff, diff_more = sqlmesh_adapter.fetchdf_with_limit(expr, limit=limit)
302
- return QueryDiffResult(
303
- diff=DataFrame.from_pandas(diff, limit=limit, more=diff_more)
350
+ """,
351
+ )
352
+ .from_("final")
353
+ .limit(1000)
304
354
  )
355
+ diff, diff_more = sqlmesh_adapter.fetchdf_with_limit(expr, limit=limit)
356
+ return QueryDiffResult(diff=DataFrame.from_pandas(diff, limit=limit, more=diff_more))
305
357
 
306
358
  def execute_sqlmesh(self):
307
359
  sql = self.params.sql_template
@@ -316,7 +368,7 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
316
368
  def execute(self):
317
369
  context = default_context()
318
370
 
319
- if context.adapter_type == 'sqlmesh':
371
+ if context.adapter_type == "sqlmesh":
320
372
  return self.execute_sqlmesh()
321
373
  else:
322
374
  return self.execute_dbt()
@@ -329,14 +381,14 @@ class QueryDiffTask(Task, QueryMixin, ValueDiffMixin):
329
381
 
330
382
  class QueryDiffResultDiffer(TaskResultDiffer):
331
383
  def _check_result_changed_fn(self, result):
332
- base = result.get('base')
333
- current = result.get('current')
334
- diff = result.get('diff')
384
+ base = result.get("base")
385
+ current = result.get("current")
386
+ diff = result.get("diff")
335
387
 
336
388
  if diff is None:
337
389
  return TaskResultDiffer.diff(base, current)
338
390
  else:
339
- diff_data = diff.get('data')
391
+ diff_data = diff.get("data")
340
392
  if diff_data is None or len(diff_data) == 0:
341
393
  return None
342
394
 
recce/tasks/rowcount.py CHANGED
@@ -1,11 +1,11 @@
1
- from typing import Optional, Union, List, Literal
1
+ from typing import List, Literal, Optional, Union
2
2
 
3
3
  from pydantic import BaseModel
4
4
 
5
5
  from recce.core import default_context
6
6
  from recce.models import Check
7
7
  from recce.tasks import Task
8
- from recce.tasks.core import TaskResultDiffer, CheckValidator
8
+ from recce.tasks.core import CheckValidator, TaskResultDiffer
9
9
  from recce.tasks.query import QueryMixin
10
10
 
11
11
 
@@ -25,10 +25,10 @@ class RowCountTask(Task, QueryMixin):
25
25
  if node is None:
26
26
  return None
27
27
 
28
- if node.resource_type != 'model' and node.resource_type != 'snapshot':
28
+ if node.resource_type != "model" and node.resource_type != "snapshot":
29
29
  return None
30
30
 
31
- if node.config and node.config.materialized not in ['table', 'view', 'incremental', 'snapshot']:
31
+ if node.config and node.config.materialized not in ["table", "view", "incremental", "snapshot"]:
32
32
  return None
33
33
 
34
34
  relation = dbt_adapter.create_relation(model_name, base=base)
@@ -54,8 +54,9 @@ class RowCountTask(Task, QueryMixin):
54
54
  for node in self.params.node_names or []:
55
55
  query_candidates.append(node)
56
56
  else:
57
+
57
58
  def countable(unique_id):
58
- return unique_id.startswith('model') or unique_id.startswith('snapshot') or unique_id.startswith('seed')
59
+ return unique_id.startswith("model") or unique_id.startswith("snapshot") or unique_id.startswith("seed")
59
60
 
60
61
  node_ids = dbt_adapter.select_nodes(
61
62
  select=self.params.select,
@@ -80,7 +81,7 @@ class RowCountTask(Task, QueryMixin):
80
81
  row_count = self._query_row_count(dbt_adapter, node, base=False)
81
82
  self.check_cancel()
82
83
  result[node] = {
83
- 'curr': row_count,
84
+ "curr": row_count,
84
85
  }
85
86
  completed += 1
86
87
 
@@ -98,7 +99,7 @@ class RowCountDiffParams(BaseModel):
98
99
  select: Optional[str] = None
99
100
  exclude: Optional[str] = None
100
101
  packages: Optional[list[str]] = None
101
- view_mode: Optional[Literal['all', 'changed_models']] = None
102
+ view_mode: Optional[Literal["all", "changed_models"]] = None
102
103
 
103
104
 
104
105
  class RowCountDiffTask(Task, QueryMixin):
@@ -112,10 +113,10 @@ class RowCountDiffTask(Task, QueryMixin):
112
113
  if node is None:
113
114
  return None
114
115
 
115
- if node.resource_type != 'model' and node.resource_type != 'snapshot':
116
+ if node.resource_type != "model" and node.resource_type != "snapshot":
116
117
  return None
117
118
 
118
- if node.config and node.config.materialized not in ['table', 'view', 'incremental', 'snapshot']:
119
+ if node.config and node.config.materialized not in ["table", "view", "incremental", "snapshot"]:
119
120
  return None
120
121
 
121
122
  relation = dbt_adapter.create_relation(model_name, base=base)
@@ -141,8 +142,9 @@ class RowCountDiffTask(Task, QueryMixin):
141
142
  for node in self.params.node_names or []:
142
143
  query_candidates.append(node)
143
144
  else:
145
+
144
146
  def countable(unique_id):
145
- return unique_id.startswith('model') or unique_id.startswith('snapshot') or unique_id.startswith('seed')
147
+ return unique_id.startswith("model") or unique_id.startswith("snapshot") or unique_id.startswith("seed")
146
148
 
147
149
  node_ids = dbt_adapter.select_nodes(
148
150
  select=self.params.select,
@@ -169,8 +171,8 @@ class RowCountDiffTask(Task, QueryMixin):
169
171
  curr_row_count = self._query_row_count(dbt_adapter, node, base=False)
170
172
  self.check_cancel()
171
173
  result[node] = {
172
- 'base': base_row_count,
173
- 'curr': curr_row_count,
174
+ "base": base_row_count,
175
+ "curr": curr_row_count,
174
176
  }
175
177
  completed += 1
176
178
 
@@ -187,6 +189,7 @@ class RowCountDiffTask(Task, QueryMixin):
187
189
  query_candidates.append(node_name)
188
190
 
189
191
  from recce.adapter.sqlmesh_adapter import SqlmeshAdapter
192
+
190
193
  sqlmesh_adapter: SqlmeshAdapter = default_context().adapter
191
194
 
192
195
  for name in query_candidates:
@@ -194,28 +197,28 @@ class RowCountDiffTask(Task, QueryMixin):
194
197
  curr_row_count = None
195
198
 
196
199
  try:
197
- df, _ = sqlmesh_adapter.fetchdf_with_limit(f'select count(*) from {name}', base=True)
200
+ df, _ = sqlmesh_adapter.fetchdf_with_limit(f"select count(*) from {name}", base=True)
198
201
  base_row_count = int(df.iloc[0, 0])
199
202
  except Exception:
200
203
  pass
201
204
  self.check_cancel()
202
205
 
203
206
  try:
204
- df, _ = sqlmesh_adapter.fetchdf_with_limit(f'select count(*) from {name}', base=False)
207
+ df, _ = sqlmesh_adapter.fetchdf_with_limit(f"select count(*) from {name}", base=False)
205
208
  curr_row_count = int(df.iloc[0, 0])
206
209
  except Exception:
207
210
  pass
208
211
  self.check_cancel()
209
212
  result[name] = {
210
- 'base': base_row_count,
211
- 'curr': curr_row_count,
213
+ "base": base_row_count,
214
+ "curr": curr_row_count,
212
215
  }
213
216
 
214
217
  return result
215
218
 
216
219
  def execute(self):
217
220
  context = default_context()
218
- if context.adapter_type == 'dbt':
221
+ if context.adapter_type == "dbt":
219
222
  return self.execute_dbt()
220
223
  else:
221
224
  return self.execute_sqlmesh()
@@ -232,8 +235,8 @@ class RowCountDiffResultDiffer(TaskResultDiffer):
232
235
  current = {}
233
236
 
234
237
  for node, row_counts in result.items():
235
- base[node] = row_counts['base']
236
- current[node] = row_counts['curr']
238
+ base[node] = row_counts["base"]
239
+ current[node] = row_counts["curr"]
237
240
 
238
241
  return TaskResultDiffer.diff(base, current)
239
242
 
@@ -243,24 +246,27 @@ class RowCountDiffResultDiffer(TaskResultDiffer):
243
246
  Should be implemented by subclass.
244
247
  """
245
248
  params = self.run.params
246
- if params.get('model'):
247
- return [TaskResultDiffer.get_node_id_by_name(params.get('model'))]
248
- elif params.get('node_names'):
249
- names = params.get('node_names', [])
249
+ if params.get("model"):
250
+ return [TaskResultDiffer.get_node_id_by_name(params.get("model"))]
251
+ elif params.get("node_names"):
252
+ names = params.get("node_names", [])
250
253
  return [TaskResultDiffer.get_node_id_by_name(name) for name in names]
251
- elif params.get('node_ids'):
252
- return params.get('node_ids', [])
254
+ elif params.get("node_ids"):
255
+ return params.get("node_ids", [])
253
256
  else:
254
257
  return TaskResultDiffer.get_node_ids_by_selector(
255
- select=params.get('select'),
256
- exclude=params.get('exclude'),
257
- packages=params.get('packages'),
258
- view_mode=params.get('view_mode'),
258
+ select=params.get("select"),
259
+ exclude=params.get("exclude"),
260
+ packages=params.get("packages"),
261
+ view_mode=params.get("view_mode"),
259
262
  )
260
263
 
261
264
  def _get_changed_nodes(self) -> Union[List[str], None]:
262
265
  if self.changes:
263
- return self.changes.affected_root_keys.items
266
+ # Both affected_root_keys of deepdiff v7 (OrderedSet) and v8 (SetOrdered) are iterable
267
+ # Convert to list directly
268
+ return list(self.changes.affected_root_keys)
269
+ return None
264
270
 
265
271
 
266
272
  class RowCountDiffCheckValidator(CheckValidator):
@@ -268,4 +274,4 @@ class RowCountDiffCheckValidator(CheckValidator):
268
274
  try:
269
275
  RowCountDiffParams(**check.params)
270
276
  except Exception as e:
271
- raise ValueError(f'Invalid params: str{e}')
277
+ raise ValueError(f"Invalid params: str{e}")
recce/tasks/schema.py CHANGED
@@ -1,9 +1,9 @@
1
- from typing import Union, List, Optional, Literal
1
+ from typing import List, Literal, Optional, Union
2
2
 
3
3
  from pydantic import BaseModel
4
4
 
5
5
  from recce.models import Check
6
- from recce.tasks.core import TaskResultDiffer, CheckValidator
6
+ from recce.tasks.core import CheckValidator, TaskResultDiffer
7
7
 
8
8
 
9
9
  class SchemaDiffResultDiffer:
@@ -17,35 +17,38 @@ class SchemaDiffResultDiffer:
17
17
 
18
18
  def _get_related_node_ids(self) -> Union[List[str], None]:
19
19
  params = self.check.params
20
- if params.get('node_id'):
21
- return params.get('node_id') if isinstance(params.get('node_id'), list) else [params.get('node_id')]
20
+ if params.get("node_id"):
21
+ return params.get("node_id") if isinstance(params.get("node_id"), list) else [params.get("node_id")]
22
22
  else:
23
23
  return TaskResultDiffer.get_node_ids_by_selector(
24
- select=params.get('select'),
25
- exclude=params.get('exclude'),
26
- packages=params.get('packages'),
27
- view_mode=params.get('view_mode'),
24
+ select=params.get("select"),
25
+ exclude=params.get("exclude"),
26
+ packages=params.get("packages"),
27
+ view_mode=params.get("view_mode"),
28
28
  )
29
29
 
30
30
  def _check_result_changed_fn(self, base_lineage, curr_lineage):
31
31
  base = {}
32
32
  current = {}
33
- base_nodes = base_lineage.get('nodes', {})
34
- curr_nodes = curr_lineage.get('nodes', {})
33
+ base_nodes = base_lineage.get("nodes", {})
34
+ curr_nodes = curr_lineage.get("nodes", {})
35
35
  for node_id in self.related_node_ids:
36
36
  node = curr_nodes.get(node_id) or base_nodes.get(node_id)
37
37
  if not node:
38
38
  continue
39
39
 
40
- node_name = node.get('name')
41
- base[node_name] = base_nodes.get(node_id, {}).get('columns', {})
42
- current[node_name] = curr_nodes.get(node_id, {}).get('columns', {})
40
+ node_name = node.get("name")
41
+ base[node_name] = base_nodes.get(node_id, {}).get("columns", {})
42
+ current[node_name] = curr_nodes.get(node_id, {}).get("columns", {})
43
43
 
44
44
  return TaskResultDiffer.diff(base, current)
45
45
 
46
46
  def _get_changed_nodes(self) -> Union[List[str], None]:
47
47
  if self.changes:
48
- return self.changes.affected_root_keys.items
48
+ # Both affected_root_keys of deepdiff v7 (OrderedSet) and v8 (SetOrdered) are iterable
49
+ # Convert to list directly
50
+ return list(self.changes.affected_root_keys)
51
+ return None
49
52
 
50
53
 
51
54
  class SchemaDiffParams(BaseModel):
@@ -53,7 +56,7 @@ class SchemaDiffParams(BaseModel):
53
56
  select: Optional[str] = None
54
57
  exclude: Optional[str] = None
55
58
  packages: Optional[list[str]] = None
56
- view_mode: Optional[Literal['all', 'changed_models']] = None
59
+ view_mode: Optional[Literal["all", "changed_models"]] = None
57
60
 
58
61
 
59
62
  class SchemaDiffCheckValidator(CheckValidator):