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
@@ -2,6 +2,7 @@ class DbtVersion:
2
2
 
3
3
  def __init__(self):
4
4
  from dbt import version as dbt_version
5
+
5
6
  dbt_version = self.parse(dbt_version.__version__)
6
7
  if dbt_version.is_prerelease:
7
8
  dbt_version = self.parse(dbt_version.base_version)
@@ -10,10 +11,12 @@ class DbtVersion:
10
11
  @staticmethod
11
12
  def parse(version: str):
12
13
  from packaging import version as v
14
+
13
15
  return v.parse(version)
14
16
 
15
17
  def as_version(self, other):
16
18
  from packaging.version import Version
19
+
17
20
  if isinstance(other, Version):
18
21
  return other
19
22
  if isinstance(other, str):
@@ -1,9 +1,9 @@
1
1
  import typing as t
2
2
  from dataclasses import dataclass
3
- from typing import Optional, Dict, Type
3
+ from typing import Dict, Optional, Type
4
4
 
5
5
  import pandas as pd
6
- from sqlglot import parse_one, Expression, select
6
+ from sqlglot import Expression, parse_one, select
7
7
  from sqlglot.optimizer import traverse_scope
8
8
  from sqlmesh.core.context import Context as SqlmeshContext
9
9
  from sqlmesh.core.environment import Environment
@@ -11,7 +11,7 @@ from sqlmesh.core.state_sync import StateReader
11
11
 
12
12
  from recce.adapter.base import BaseAdapter
13
13
  from recce.models import RunType
14
- from recce.tasks import Task, QueryTask, QueryDiffTask, RowCountDiffTask
14
+ from recce.tasks import QueryDiffTask, QueryTask, RowCountDiffTask, Task
15
15
 
16
16
  sqlmesh_supported_registry: Dict[RunType, Type[Task]] = {
17
17
  RunType.QUERY: QueryTask,
@@ -37,7 +37,7 @@ class SqlmeshAdapter(BaseAdapter):
37
37
 
38
38
  for snapshot in state_reader.get_snapshots(env.snapshots, hydrate_seeds=True).values():
39
39
 
40
- if snapshot.node_type.lower() != 'model':
40
+ if snapshot.node_type.lower() != "model":
41
41
  continue
42
42
 
43
43
  model = snapshot.model
@@ -45,16 +45,16 @@ class SqlmeshAdapter(BaseAdapter):
45
45
  continue
46
46
 
47
47
  node = {
48
- 'unique_id': model.name,
49
- 'name': model.name,
50
- 'resource_type': snapshot.node_type.lower(),
51
- 'checksum': {'checksum': snapshot.fingerprint.data_hash},
48
+ "unique_id": model.name,
49
+ "name": model.name,
50
+ "resource_type": snapshot.node_type.lower(),
51
+ "checksum": {"checksum": snapshot.fingerprint.data_hash},
52
52
  }
53
53
 
54
54
  columns = {}
55
55
  for column, type in model.columns_to_types.items():
56
- columns[column] = {'name': column, 'type': str(type)}
57
- node['columns'] = columns
56
+ columns[column] = {"name": column, "type": str(type)}
57
+ node["columns"] = columns
58
58
 
59
59
  nodes[snapshot.name] = node
60
60
  parents = [snapshotId.name for snapshotId in snapshot.parents]
@@ -81,15 +81,15 @@ class SqlmeshAdapter(BaseAdapter):
81
81
 
82
82
  @classmethod
83
83
  def load(cls, **kwargs):
84
- sqlmesh_envs = kwargs.get('sqlmesh_envs')
84
+ sqlmesh_envs = kwargs.get("sqlmesh_envs")
85
85
  if sqlmesh_envs is None:
86
- raise Exception('\'--sqlmesh-envs SOURCE:TARGET\' is required')
86
+ raise Exception("'--sqlmesh-envs SOURCE:TARGET' is required")
87
87
 
88
- envs = sqlmesh_envs.split(':')
88
+ envs = sqlmesh_envs.split(":")
89
89
  if len(envs) != 2:
90
90
  raise Exception('sqlmesh_envs must be in the format of "SOURCE:TARGET"')
91
91
 
92
- sqlmesh_config = kwargs.get('sqlmesh_config', None)
92
+ sqlmesh_config = kwargs.get("sqlmesh_config", None)
93
93
  context = SqlmeshContext(config=sqlmesh_config)
94
94
  base_env = context.state_reader.get_environment(envs[0])
95
95
  curr_env = context.state_reader.get_environment(envs[1])
@@ -100,18 +100,14 @@ class SqlmeshAdapter(BaseAdapter):
100
100
 
101
101
  return cls(context=context, base_env=base_env, curr_env=curr_env)
102
102
 
103
- def replace_virtual_tables(
104
- self,
105
- sql: t.Union[Expression, str],
106
- base: bool = None
107
- ) -> Expression:
108
- '''
103
+ def replace_virtual_tables(self, sql: t.Union[Expression, str], base: bool = None) -> Expression:
104
+ """
109
105
  Replace virtual tables based on the env name.
110
106
 
111
107
  Args:
112
108
  sql: SQL expression to replace virtual tables
113
109
  base: True: replace virtual tables with base env, False: replace virtual tables with current env, None: no replacement
114
- '''
110
+ """
115
111
  if isinstance(sql, str):
116
112
  expression = parse_one(sql, dialect=self.context.default_dialect)
117
113
  else:
@@ -119,30 +115,23 @@ class SqlmeshAdapter(BaseAdapter):
119
115
 
120
116
  if base is not None:
121
117
  env = self.base_env if base else self.curr_env
122
- if env.name != 'prod':
118
+ if env.name != "prod":
123
119
  model_names = [model.name for model in self.context.models.values()]
124
120
  for scope in traverse_scope(expression):
125
121
  for table in scope.tables:
126
- if f'{table.db}.{table.name}' in model_names:
127
- table.args['db'] = f"{table.args['db']}__{env.name}"
122
+ if f"{table.db}.{table.name}" in model_names:
123
+ table.args["db"] = f"{table.args['db']}__{env.name}"
128
124
 
129
125
  return expression
130
126
 
131
127
  def fetchdf_with_limit(
132
- self,
133
- sql: t.Union[Expression, str],
134
- base: Optional[bool] = None,
135
- limit: Optional[int] = None
128
+ self, sql: t.Union[Expression, str], base: Optional[bool] = None, limit: Optional[int] = None
136
129
  ) -> (pd.DataFrame, bool):
137
130
  expression = self.replace_virtual_tables(sql, base=base)
138
131
  if limit:
139
- expression = select(
140
- '*'
141
- ).from_(
142
- '__QUERY'
143
- ).with_(
144
- '__QUERY', as_=expression
145
- ).limit(limit + 1 if limit else None)
132
+ expression = (
133
+ select("*").from_("__QUERY").with_("__QUERY", as_=expression).limit(limit + 1 if limit else None)
134
+ )
146
135
  df = self.context.fetchdf(expression)
147
136
  if limit and len(df) > limit:
148
137
  df = df.head(limit)
recce/apis/check_api.py CHANGED
@@ -2,23 +2,27 @@ from datetime import datetime, timezone
2
2
  from typing import Optional
3
3
  from uuid import UUID
4
4
 
5
- from fastapi import APIRouter, HTTPException, BackgroundTasks
5
+ from fastapi import APIRouter, BackgroundTasks, HTTPException
6
6
  from pydantic import BaseModel
7
7
 
8
- from recce.apis.check_func import create_check_without_run, create_check_from_run, export_persistent_state
8
+ from recce.apis.check_func import (
9
+ create_check_from_run,
10
+ create_check_without_run,
11
+ export_persistent_state,
12
+ )
9
13
  from recce.apis.run_func import submit_run
10
14
  from recce.event import log_api_event
11
15
  from recce.exceptions import RecceException
12
- from recce.models import RunType, RunDAO, Check, CheckDAO, Run
16
+ from recce.models import Check, CheckDAO, Run, RunDAO, RunType
13
17
 
14
- check_router = APIRouter(tags=['check'])
18
+ check_router = APIRouter(tags=["check"])
15
19
 
16
20
 
17
21
  class CreateCheckIn(BaseModel):
18
22
  name: Optional[str] = None
19
- description: str = ''
23
+ description: str = ""
20
24
  run_id: Optional[str] = None
21
- type: Optional[RunType] = None,
25
+ type: Optional[RunType] = (None,)
22
26
  params: Optional[dict] = None
23
27
  view_options: Optional[dict] = None
24
28
  track_props: Optional[dict] = None
@@ -39,16 +43,17 @@ class CheckOut(BaseModel):
39
43
  def from_check(cls, check: Check):
40
44
  check_related_runs = RunDAO().list_by_check_id(check.check_id)
41
45
  last_run = check_related_runs[-1] if len(check_related_runs) > 0 else None
42
- return CheckOut(check_id=check.check_id,
43
- name=check.name,
44
- description=check.description,
45
- type=check.type.value,
46
- params=check.params,
47
- view_options=check.view_options,
48
- is_checked=check.is_checked,
49
- is_preset=check.is_preset,
50
- last_run=last_run,
51
- )
46
+ return CheckOut(
47
+ check_id=check.check_id,
48
+ name=check.name,
49
+ description=check.description,
50
+ type=check.type.value,
51
+ params=check.params,
52
+ view_options=check.view_options,
53
+ is_checked=check.is_checked,
54
+ is_preset=check.is_preset,
55
+ last_run=last_run,
56
+ )
52
57
 
53
58
 
54
59
  @check_router.post("/checks", status_code=201, response_model=CheckOut)
@@ -59,7 +64,7 @@ async def create_check(check_in: CreateCheckIn, background_tasks: BackgroundTask
59
64
  check_in.run_id,
60
65
  check_name=check_in.name,
61
66
  check_description=check_in.description,
62
- check_view_options=check_in.view_options
67
+ check_view_options=check_in.view_options,
63
68
  )
64
69
  else:
65
70
  check = create_check_without_run(
@@ -67,12 +72,15 @@ async def create_check(check_in: CreateCheckIn, background_tasks: BackgroundTask
67
72
  check_description=check_in.description,
68
73
  check_type=check_in.type,
69
74
  params=check_in.params,
70
- check_view_options=check_in.view_options
75
+ check_view_options=check_in.view_options,
71
76
  )
72
- log_api_event('create_check', dict(
73
- type=str(check.type),
74
- track_props=check_in.track_props,
75
- ))
77
+ log_api_event(
78
+ "create_check",
79
+ dict(
80
+ type=str(check.type),
81
+ track_props=check_in.track_props,
82
+ ),
83
+ )
76
84
  except NameError as e:
77
85
  raise HTTPException(status_code=404, detail=str(e))
78
86
  except ValueError as e:
@@ -93,10 +101,13 @@ async def run_check_handler(check_id: UUID, input: RunCheckIn):
93
101
  raise HTTPException(status_code=404, detail=f"Check ID '{check_id}' not found")
94
102
 
95
103
  try:
96
- log_api_event('rerun_check', dict(
97
- type=str(check.type),
98
- rerun=True,
99
- ))
104
+ log_api_event(
105
+ "rerun_check",
106
+ dict(
107
+ type=str(check.type),
108
+ rerun=True,
109
+ ),
110
+ )
100
111
  run, future = submit_run(check.type, check.params, check_id=check_id)
101
112
  except RecceException as e:
102
113
  raise HTTPException(status_code=400, detail=str(e))
@@ -122,7 +133,7 @@ async def list_checks_handler():
122
133
  async def get_check_handler(check_id: UUID):
123
134
  check = CheckDAO().find_check_by_id(check_id)
124
135
  if check is None:
125
- raise HTTPException(status_code=404, detail='Not Found')
136
+ raise HTTPException(status_code=404, detail="Not Found")
126
137
 
127
138
  runs = RunDAO().list_by_check_id(check_id)
128
139
  last_run = runs[-1] if len(runs) > 0 else None
@@ -144,7 +155,7 @@ class PatchCheckIn(BaseModel):
144
155
  async def update_check_handler(check_id: UUID, patch: PatchCheckIn, background_tasks: BackgroundTasks):
145
156
  check = CheckDAO().find_check_by_id(check_id)
146
157
  if check is None:
147
- raise HTTPException(status_code=404, detail='Not Found')
158
+ raise HTTPException(status_code=404, detail="Not Found")
148
159
 
149
160
  if patch.name is not None:
150
161
  check.name = patch.name
recce/apis/check_func.py CHANGED
@@ -5,13 +5,13 @@ from fastapi import HTTPException
5
5
 
6
6
  from recce.apis.run_func import generate_run_name
7
7
  from recce.core import default_context
8
- from recce.models import RunDAO, RunType, Check, CheckDAO
8
+ from recce.models import Check, CheckDAO, RunDAO, RunType
9
9
 
10
10
 
11
11
  def validate_schema_diff_check(params):
12
- node_id = params.get('node_id')
12
+ node_id = params.get("node_id")
13
13
  if node_id is None:
14
- raise HTTPException(status_code=400, detail='node_id is required for schema diff')
14
+ raise HTTPException(status_code=400, detail="node_id is required for schema diff")
15
15
  node_name = default_context().get_node_name_by_id(node_id)
16
16
  if node_name is None:
17
17
  raise HTTPException(status_code=400, detail=f"node_id '{node_id}' not found in dbt manifest")
@@ -45,8 +45,8 @@ def _get_ref_model(sql_template: str) -> Optional[str]:
45
45
  def _generate_check_name(check_type, params, view_options):
46
46
  now = datetime.utcnow().strftime("%d %b %Y")
47
47
  if check_type == RunType.SCHEMA_DIFF:
48
- if params.get('node_id'):
49
- nodeIds = params.get('node_id') if isinstance(params.get('node_id'), list) else [params.get('node_id')]
48
+ if params.get("node_id"):
49
+ nodeIds = params.get("node_id") if isinstance(params.get("node_id"), list) else [params.get("node_id")]
50
50
  if len(nodeIds) == 1:
51
51
  node_name = get_node_name_by_id(nodeIds[0])
52
52
  return f"schema diff of {node_name}".capitalize()
@@ -54,7 +54,7 @@ def _generate_check_name(check_type, params, view_options):
54
54
  return f"schema diff of {len(nodeIds)} nodes".capitalize()
55
55
  return f"{'schema diff'.capitalize()} - {now}"
56
56
  elif check_type == RunType.LINEAGE_DIFF:
57
- nodes = view_options.get('node_ids') if view_options else params.get('node_ids')
57
+ nodes = view_options.get("node_ids") if view_options else params.get("node_ids")
58
58
  if nodes is not None:
59
59
  return f"lineage diff of {len(nodes)} nodes".capitalize()
60
60
  return f"{'lineage diff'.capitalize()} - {now}"
@@ -62,10 +62,11 @@ def _generate_check_name(check_type, params, view_options):
62
62
  return f"{'check'.capitalize()} - {now}"
63
63
 
64
64
 
65
- def create_check_from_run(run_id, check_name=None, check_description='', check_view_options=None, is_preset=False,
66
- is_checked=False):
65
+ def create_check_from_run(
66
+ run_id, check_name=None, check_description="", check_view_options=None, is_preset=False, is_checked=False
67
+ ):
67
68
  if run_id is None:
68
- raise ValueError('run_id is required')
69
+ raise ValueError("run_id is required")
69
70
 
70
71
  run = RunDAO().find_run_by_id(run_id)
71
72
  if run is None:
@@ -76,29 +77,34 @@ def create_check_from_run(run_id, check_name=None, check_description='', check_v
76
77
 
77
78
  _validate_check(run_type, run_params)
78
79
  name = check_name if check_name is not None else generate_run_name(run)
79
- check = Check(name=name,
80
- description=check_description,
81
- type=run_type,
82
- params=run_params,
83
- view_options=check_view_options,
84
- is_preset=is_preset,
85
- is_checked=is_checked)
80
+ check = Check(
81
+ name=name,
82
+ description=check_description,
83
+ type=run_type,
84
+ params=run_params,
85
+ view_options=check_view_options,
86
+ is_preset=is_preset,
87
+ is_checked=is_checked,
88
+ )
86
89
  CheckDAO().create(check)
87
90
  run.check_id = check.check_id
88
91
 
89
92
  return check
90
93
 
91
94
 
92
- def create_check_without_run(check_name, check_description, check_type, params, check_view_options, is_preset=False,
93
- is_checked=False):
95
+ def create_check_without_run(
96
+ check_name, check_description, check_type, params, check_view_options, is_preset=False, is_checked=False
97
+ ):
94
98
  name = check_name if check_name is not None else _generate_check_name(check_type, params, check_view_options)
95
- check = Check(name=name,
96
- description=check_description,
97
- type=check_type,
98
- params=params,
99
- view_options=check_view_options,
100
- is_preset=is_preset,
101
- is_checked=is_checked)
99
+ check = Check(
100
+ name=name,
101
+ description=check_description,
102
+ type=check_type,
103
+ params=params,
104
+ view_options=check_view_options,
105
+ is_preset=is_preset,
106
+ is_checked=is_checked,
107
+ )
102
108
  CheckDAO().create(check)
103
109
  return check
104
110
 
@@ -119,6 +125,6 @@ def export_persistent_state():
119
125
  if state_loader is not None:
120
126
  is_conflict = state_loader.check_conflict()
121
127
  if is_conflict:
122
- ctx.sync_state('merge')
128
+ ctx.sync_state("merge")
123
129
  else:
124
- ctx.sync_state('overwrite')
130
+ ctx.sync_state("overwrite")
recce/apis/run_api.py CHANGED
@@ -1,16 +1,16 @@
1
1
  import asyncio
2
- from typing import Optional, List
2
+ from typing import List, Optional
3
3
  from uuid import UUID
4
4
 
5
5
  from fastapi import APIRouter, HTTPException, Query
6
6
  from pydantic import BaseModel
7
7
 
8
- from recce.apis.run_func import submit_run, cancel_run, materialize_run_results
8
+ from recce.apis.run_func import cancel_run, materialize_run_results, submit_run
9
9
  from recce.event import log_api_event
10
10
  from recce.exceptions import RecceException
11
11
  from recce.models import RunDAO
12
12
 
13
- run_router = APIRouter(tags=['run'])
13
+ run_router = APIRouter(tags=["run"])
14
14
 
15
15
 
16
16
  class CreateRunIn(BaseModel):
@@ -23,10 +23,13 @@ class CreateRunIn(BaseModel):
23
23
 
24
24
  @run_router.post("/runs", status_code=201)
25
25
  async def create_run_handler(input: CreateRunIn):
26
- log_api_event('create_run', dict(
27
- type=input.type,
28
- track_props=input.track_props,
29
- ))
26
+ log_api_event(
27
+ "create_run",
28
+ dict(
29
+ type=input.type,
30
+ track_props=input.track_props,
31
+ ),
32
+ )
30
33
  try:
31
34
  run, future = submit_run(input.type, input.params)
32
35
  except RecceException as e:
@@ -51,7 +54,7 @@ async def cancel_run_handler(run_id: UUID):
51
54
  async def wait_run_handler(run_id: UUID, timeout: int = Query(None, description="Maximum number of seconds to wait")):
52
55
  run = RunDAO().find_run_by_id(run_id)
53
56
  if run is None:
54
- raise HTTPException(status_code=404, detail='Not Found')
57
+ raise HTTPException(status_code=404, detail="Not Found")
55
58
 
56
59
  start_time = asyncio.get_event_loop().time()
57
60
  while run.result is None and run.error is None:
@@ -65,18 +68,21 @@ async def wait_run_handler(run_id: UUID, timeout: int = Query(None, description=
65
68
  async def list_run_handler():
66
69
  runs = RunDAO().list() or []
67
70
 
68
- result = [{
69
- 'run_id': run.run_id,
70
- 'run_at': run.run_at,
71
- 'name': run.name,
72
- 'type': run.type,
73
- 'params': run.params,
74
- 'status': run.status,
75
- 'check_id': run.check_id,
76
- } for run in runs]
71
+ result = [
72
+ {
73
+ "run_id": run.run_id,
74
+ "run_at": run.run_at,
75
+ "name": run.name,
76
+ "type": run.type,
77
+ "params": run.params,
78
+ "status": run.status,
79
+ "check_id": run.check_id,
80
+ }
81
+ for run in runs
82
+ ]
77
83
 
78
84
  # sort by run_at
79
- result = sorted(result, key=lambda x: x['run_at'], reverse=True)
85
+ result = sorted(result, key=lambda x: x["run_at"], reverse=True)
80
86
 
81
87
  return result
82
88
 
@@ -101,7 +107,7 @@ async def search_runs_handler(search: SearchRunsIn):
101
107
  result.append(run)
102
108
 
103
109
  if search.limit:
104
- return result[-search.limit:]
110
+ return result[-search.limit :]
105
111
 
106
112
  return result
107
113
 
recce/apis/run_func.py CHANGED
@@ -4,11 +4,11 @@ from typing import List, Optional
4
4
 
5
5
  from recce.core import default_context
6
6
  from recce.exceptions import RecceException
7
- from recce.models import RunType, Run, RunDAO
7
+ from recce.models import Run, RunDAO, RunType
8
8
  from recce.models.types import RunStatus
9
9
 
10
10
  running_tasks = {}
11
- logger = logging.getLogger('uvicorn')
11
+ logger = logging.getLogger("uvicorn")
12
12
 
13
13
 
14
14
  def _get_ref_model(sql_template: str) -> Optional[str]:
@@ -33,26 +33,26 @@ def generate_run_name(run):
33
33
  now = dateutil.parser.parse(run.run_at)
34
34
 
35
35
  if run_type == RunType.QUERY:
36
- ref = _get_ref_model(params.get('sql_template'))
36
+ ref = _get_ref_model(params.get("sql_template"))
37
37
  if ref:
38
38
  return f"query of {ref}".capitalize()
39
39
  return f"{'query'.capitalize()} - {now}"
40
40
  elif run_type == RunType.QUERY_DIFF:
41
- ref = _get_ref_model(params.get('sql_template'))
41
+ ref = _get_ref_model(params.get("sql_template"))
42
42
  if ref:
43
43
  return f"query diff of {ref}".capitalize()
44
44
  return f"{'query diff'.capitalize()} - {now}"
45
45
  elif run_type == RunType.VALUE_DIFF:
46
- model = params.get('model')
46
+ model = params.get("model")
47
47
  return f"value diff of {model}".capitalize()
48
48
  elif run_type == RunType.VALUE_DIFF_DETAIL:
49
- model = params.get('model')
49
+ model = params.get("model")
50
50
  return f"value diff detail of {model}".capitalize()
51
51
  elif run_type == RunType.PROFILE_DIFF:
52
- model = params.get('model')
52
+ model = params.get("model")
53
53
  return f"profile diff of {model}".capitalize()
54
54
  elif run_type == RunType.ROW_COUNT_DIFF:
55
- nodes = params.get('node_names')
55
+ nodes = params.get("node_names")
56
56
  if nodes:
57
57
  if len(nodes) == 1:
58
58
  node = nodes[0]
@@ -62,23 +62,27 @@ def generate_run_name(run):
62
62
  else:
63
63
  return "row count of multiple nodes".capitalize()
64
64
  elif run_type == RunType.TOP_K_DIFF:
65
- model = params.get('model')
66
- column = params.get('column_name')
65
+ model = params.get("model")
66
+ column = params.get("column_name")
67
67
  return f"top-k diff of {model}.{column} ".capitalize()
68
68
  elif run_type == RunType.HISTOGRAM_DIFF:
69
- model = params.get('model')
70
- column = params.get('column_name')
69
+ model = params.get("model")
70
+ column = params.get("column_name")
71
71
  return f"histogram diff of {model}.{column} ".capitalize()
72
72
  else:
73
73
  return f"{'run'.capitalize()} - {now}"
74
74
 
75
75
 
76
76
  def create_task(run_type: RunType, params: dict):
77
- if default_context().adapter_type == 'sqlmesh':
78
- from recce.adapter.sqlmesh_adapter import sqlmesh_supported_registry as sqlmesh_registry
77
+ if default_context().adapter_type == "sqlmesh":
78
+ from recce.adapter.sqlmesh_adapter import (
79
+ sqlmesh_supported_registry as sqlmesh_registry,
80
+ )
81
+
79
82
  registry = sqlmesh_registry
80
83
  else:
81
84
  from recce.adapter.dbt_adapter import dbt_supported_registry as dbt_registry
85
+
82
86
  registry = dbt_registry
83
87
 
84
88
  taskClz = registry.get(run_type)
@@ -101,6 +105,7 @@ def submit_run(type, params, check_id=None):
101
105
  context = default_context()
102
106
  if context.review_mode is True:
103
107
  from recce.adapter.dbt_adapter import DbtAdapter
108
+
104
109
  dbt_adaptor: DbtAdapter = context.adapter
105
110
  if dbt_adaptor.adapter is None:
106
111
  raise RecceException("Recce Server is not launched under DBT project folder.")
@@ -113,7 +118,7 @@ def submit_run(type, params, check_id=None):
113
118
  running_tasks[run.run_id] = task
114
119
 
115
120
  def progress_listener(message=None, percentage=None):
116
- run.progress = {'message': message, 'percentage': percentage}
121
+ run.progress = {"message": message, "percentage": percentage}
117
122
 
118
123
  task.progress_listener = progress_listener
119
124
 
@@ -124,7 +129,7 @@ def submit_run(type, params, check_id=None):
124
129
  run.result = result
125
130
  run.status = RunStatus.FINISHED
126
131
  if error is not None:
127
- failed_reason = str(error) if str(error) != 'None' else repr(error)
132
+ failed_reason = str(error) if str(error) != "None" else repr(error)
128
133
  run.error = failed_reason
129
134
  if run.status != RunStatus.CANCELLED:
130
135
  run.status = RunStatus.FAILED
@@ -140,9 +145,10 @@ def submit_run(type, params, check_id=None):
140
145
  if isinstance(e, RecceException) and e.is_raise is False:
141
146
  return None
142
147
  import sentry_sdk
148
+
143
149
  sentry_sdk.capture_exception(e)
144
- failed_reason = str(e) if str(e) != 'None' else repr(e)
145
- failed_reason = failed_reason.replace('. ', ".\n")
150
+ failed_reason = str(e) if str(e) != "None" else repr(e)
151
+ failed_reason = failed_reason.replace(". ", ".\n")
146
152
  logger.error(f"Failed to execute {run_type} task: {failed_reason}")
147
153
  return None
148
154
 
@@ -164,7 +170,7 @@ def cancel_run(run_id):
164
170
 
165
171
 
166
172
  def materialize_run_results(runs: List[Run], nodes: List[str] = None):
167
- '''
173
+ """
168
174
  Materialize the run results for nodes. It walks through all runs and get the last results for primary run types.
169
175
 
170
176
  The result format
@@ -180,11 +186,11 @@ def materialize_run_results(runs: List[Run], nodes: List[str] = None):
180
186
  },
181
187
  },
182
188
  }
183
- '''
189
+ """
184
190
 
185
191
  context = default_context()
186
192
  if context:
187
- mame_to_unique_id = context.build_name_to_unique_id_index(excluded_types={'semantic_model', 'metric'})
193
+ mame_to_unique_id = context.build_name_to_unique_id_index(excluded_types={"semantic_model", "metric"})
188
194
  else:
189
195
  mame_to_unique_id = {}
190
196
 
@@ -205,7 +211,7 @@ def materialize_run_results(runs: List[Run], nodes: List[str] = None):
205
211
  node_result = result[key] = {}
206
212
  else:
207
213
  node_result = result.get(key)
208
- node_result['row_count_diff'] = {'run_id': run.run_id, 'result': node_run_result}
214
+ node_result["row_count_diff"] = {"run_id": run.run_id, "result": node_run_result}
209
215
  elif run.type == RunType.ROW_COUNT:
210
216
  for model_name, node_run_result in run.result.items():
211
217
  key = mame_to_unique_id.get(model_name, model_name)
@@ -218,5 +224,5 @@ def materialize_run_results(runs: List[Run], nodes: List[str] = None):
218
224
  node_result = result[key] = {}
219
225
  else:
220
226
  node_result = result.get(key)
221
- node_result['row_count'] = {'run_id': run.run_id, 'result': node_run_result}
227
+ node_result["row_count"] = {"run_id": run.run_id, "result": node_run_result}
222
228
  return result