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
recce/event/collector.py CHANGED
@@ -4,20 +4,19 @@ import platform
4
4
  import sys
5
5
  import time
6
6
  from contextlib import contextmanager
7
- from datetime import datetime
7
+ from datetime import datetime, timezone
8
8
  from json import JSONDecodeError
9
- from datetime import timezone
10
9
 
11
10
  import portalocker
12
11
  import requests
13
12
 
14
- from recce import __version__, is_ci_env
13
+ from recce import __version__, is_ci_env, is_recce_cloud_instance
15
14
  from recce.github import is_github_codespace
16
15
 
17
16
 
18
17
  class Collector:
19
18
  def __init__(self):
20
- self._api_endpoint = 'https://api.amplitude.com/2/httpapi'
19
+ self._api_endpoint = "https://api.amplitude.com/2/httpapi"
21
20
  self._api_key = None
22
21
  self._user_id = None
23
22
 
@@ -26,6 +25,7 @@ class Collector:
26
25
  self._upload_threshold = 10
27
26
  self._is_ci: bool = is_ci_env()
28
27
  self._is_github_codespace: bool = is_github_codespace()
28
+ self._is_recce_cloud_instance: bool = is_recce_cloud_instance()
29
29
  self._flush_timer = None
30
30
 
31
31
  def schedule_flush(self):
@@ -35,6 +35,7 @@ class Collector:
35
35
 
36
36
  # send async thread
37
37
  import threading
38
+
38
39
  if self._flush_timer:
39
40
  try:
40
41
  self._flush_timer.cancel()
@@ -59,11 +60,18 @@ class Collector:
59
60
  self._unsend_events_file = unsend_events_file
60
61
  self._check_required_files()
61
62
 
62
- def _log_event(self, user_id, event_type, created_at, user_properties, event_properties, ):
63
+ def _log_event(
64
+ self,
65
+ user_id,
66
+ event_type,
67
+ created_at,
68
+ user_properties,
69
+ event_properties,
70
+ ):
63
71
  event = dict(
64
72
  user_id=user_id,
65
73
  event_type=event_type,
66
- ip='$remote',
74
+ ip="$remote",
67
75
  time=int(time.mktime(created_at.timetuple())),
68
76
  user_properties=user_properties,
69
77
  event_properties=event_properties,
@@ -91,7 +99,7 @@ class Collector:
91
99
  else:
92
100
  # Convert to UTC timezone
93
101
  created_at = event_triggered_at.astimezone(timezone.utc)
94
- python_version = f'{sys.version_info.major}.{sys.version_info.minor}'
102
+ python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
95
103
 
96
104
  # when the recce is running in automation use cases
97
105
  # replace the user id with project_id to avoid so many unique user id
@@ -102,6 +110,7 @@ class Collector:
102
110
  python_version=python_version,
103
111
  is_ci=self._is_ci,
104
112
  is_github_codespace=self._is_github_codespace,
113
+ is_recce_cloud_instance=self._is_recce_cloud_instance,
105
114
  )
106
115
 
107
116
  if user_properties is not None:
@@ -120,17 +129,18 @@ class Collector:
120
129
  if not os.path.exists(user_home):
121
130
  os.makedirs(user_home, exist_ok=True)
122
131
  if not os.path.exists(self._unsend_events_file):
123
- with portalocker.Lock(self._unsend_events_file, 'w+', timeout=5) as f:
124
- f.write(json.dumps({'unsend_events': []}))
132
+ with portalocker.Lock(self._unsend_events_file, "w+", timeout=5) as f:
133
+ f.write(json.dumps({"unsend_events": []}))
125
134
 
126
135
  def _is_full(self):
127
- with portalocker.Lock(self._unsend_events_file, 'r+', timeout=5) as f:
136
+ with portalocker.Lock(self._unsend_events_file, "r+", timeout=5) as f:
128
137
  o = json.loads(f.read())
129
- return len(o.get('unsend_events', [])) >= self._upload_threshold
138
+ return len(o.get("unsend_events", [])) >= self._upload_threshold
130
139
 
131
140
  @contextmanager
132
141
  def load_json(self):
133
- with portalocker.Lock(self._unsend_events_file, 'r+', timeout=5) as f:
142
+ with portalocker.Lock(self._unsend_events_file, "r+", timeout=5) as f:
143
+ o = None
134
144
  try:
135
145
  o = json.loads(f.read())
136
146
  yield o
@@ -140,15 +150,16 @@ class Collector:
140
150
  finally:
141
151
  f.seek(0)
142
152
  f.truncate()
143
- f.write(json.dumps(o))
153
+ if o is not None:
154
+ f.write(json.dumps(o))
144
155
 
145
156
  def send_events(self):
146
157
  with self.load_json() as o:
147
158
  payload = dict(
148
159
  api_key=self._api_key,
149
- events=o['unsend_events'],
160
+ events=o["unsend_events"],
150
161
  )
151
- o['unsend_events'] = []
162
+ o["unsend_events"] = []
152
163
  try:
153
164
  requests.post(self._api_endpoint, json=payload)
154
165
  except Exception:
@@ -157,17 +168,17 @@ class Collector:
157
168
 
158
169
  def _store_to_file(self, event):
159
170
  with self.load_json() as o:
160
- events = o.get('unsend_events', None)
171
+ events = o.get("unsend_events", None)
161
172
  if events is None:
162
- o['unsend_events'] = []
173
+ o["unsend_events"] = []
163
174
 
164
- o['unsend_events'].append(event)
175
+ o["unsend_events"].append(event)
165
176
 
166
177
  def _cleanup_unsend_events(self):
167
178
  with self.load_json() as o:
168
- events = o.get('unsend_events', None)
179
+ events = o.get("unsend_events", None)
169
180
  if events is None:
170
- o['unsend_events'] = []
181
+ o["unsend_events"] = []
171
182
 
172
- while len(o['unsend_events']) > self._delete_threshold:
173
- o['unsend_events'].pop(0)
183
+ while len(o["unsend_events"]) > self._delete_threshold:
184
+ o["unsend_events"].pop(0)
recce/event/track.py CHANGED
@@ -18,8 +18,8 @@ from recce.git import current_branch, hosting_repo
18
18
 
19
19
  console = Console()
20
20
 
21
- _enable_traceback: bool = os.environ.get('RECCE_PRINT_TRACEBACK') == '1'
22
- logger = logging.getLogger('uvicorn')
21
+ _enable_traceback: bool = os.environ.get("RECCE_PRINT_TRACEBACK") == "1"
22
+ logger = logging.getLogger("uvicorn")
23
23
 
24
24
 
25
25
  class TrackCommand(Command):
@@ -39,11 +39,23 @@ class TrackCommand(Command):
39
39
  deprecated: bool = False,
40
40
  beta: bool = False,
41
41
  ) -> None:
42
- super(TrackCommand, self).__init__(name, context_settings, callback, params, help, epilog, short_help,
43
- options_metavar, add_help_option, no_args_is_help, hidden, deprecated)
42
+ super(TrackCommand, self).__init__(
43
+ name,
44
+ context_settings,
45
+ callback,
46
+ params,
47
+ help,
48
+ epilog,
49
+ short_help,
50
+ options_metavar,
51
+ add_help_option,
52
+ no_args_is_help,
53
+ hidden,
54
+ deprecated,
55
+ )
44
56
 
45
57
  def _show_error_message(self, msg, params):
46
- if params.get('debug'):
58
+ if params.get("debug"):
47
59
  console.print_exception(show_locals=True)
48
60
  else:
49
61
  print(traceback.format_exc())
@@ -51,39 +63,39 @@ class TrackCommand(Command):
51
63
  # console.out(msg, highlight=False)
52
64
 
53
65
  def _show_hint_message(self, hint):
54
- console.print(f'[bold yellow]Hint[/bold yellow]:\n {escape(hint)}')
66
+ console.print(f"[bold yellow]Hint[/bold yellow]:\n {escape(hint)}")
55
67
 
56
68
  def invoke(self, ctx: Context) -> t.Any:
57
69
  status = False
58
70
  start_time = time.time()
59
- reason = 'error'
60
- event.set_exception_tag('command', ctx.command.name)
71
+ reason = "error"
72
+ event.set_exception_tag("command", ctx.command.name)
61
73
  event.log_codespaces_events(ctx.command.name)
62
74
 
63
75
  try:
64
76
  ret = super(TrackCommand, self).invoke(ctx)
65
77
  if ret is None or ret == 0:
66
78
  status = True
67
- reason = 'ok'
79
+ reason = "ok"
68
80
  else:
69
- reason = 'error'
81
+ reason = "error"
70
82
  sys.exit(ret)
71
83
  return ret
72
84
  except RecceException as e:
73
85
  logger.debug(traceback.format_exc())
74
86
  console.log("[Error] " + str(e))
75
- reason = 'error'
87
+ reason = "error"
76
88
  sys.exit(1)
77
89
  except SystemExit as e:
78
- reason = 'error'
90
+ reason = "error"
79
91
  raise e
80
92
  except KeyboardInterrupt as e:
81
- reason = 'aborted'
93
+ reason = "aborted"
82
94
  raise e
83
95
  except Exception as e:
84
96
  self._show_error_message(str(e), ctx.params)
85
97
  event.capture_exception(e)
86
- reason = 'fatal'
98
+ reason = "fatal"
87
99
  event.flush_exceptions()
88
100
  sys.exit(1)
89
101
  finally:
@@ -93,32 +105,32 @@ class TrackCommand(Command):
93
105
  branch = current_branch()
94
106
  command = ctx.command.name
95
107
  duration = end_time - start_time
96
- target_path = ctx.params.get('target_path', None)
97
- target_base_path = ctx.params.get('target_base_path', None)
108
+ target_path = ctx.params.get("target_path", None)
109
+ target_base_path = ctx.params.get("target_base_path", None)
98
110
  props = dict(
99
111
  command=command,
100
112
  status=status,
101
113
  reason=reason,
102
114
  duration=duration,
103
- cloud=ctx.params.get('cloud', False),
104
- review=ctx.params.get('review', False),
105
- debug=ctx.params.get('debug', False),
115
+ cloud=ctx.params.get("cloud", False),
116
+ review=ctx.params.get("review", False),
117
+ debug=ctx.params.get("debug", False),
106
118
  )
107
119
 
108
120
  if runner is not None:
109
- props['runner_type'] = runner
121
+ props["runner_type"] = runner
110
122
 
111
123
  if repo is not None:
112
- props['repository'] = sha256(repo.encode()).hexdigest()
124
+ props["repository"] = sha256(repo.encode()).hexdigest()
113
125
 
114
126
  if branch is not None:
115
- props['branch'] = sha256(branch.encode()).hexdigest()
127
+ props["branch"] = sha256(branch.encode()).hexdigest()
116
128
 
117
129
  if target_path is not None:
118
- props['target_path'] = sha256(target_path.encode()).hexdigest()
130
+ props["target_path"] = sha256(target_path.encode()).hexdigest()
119
131
 
120
132
  if target_base_path is not None:
121
- props['target_base_path'] = sha256(target_base_path.encode()).hexdigest()
133
+ props["target_base_path"] = sha256(target_base_path.encode()).hexdigest()
122
134
 
123
135
  try:
124
136
  recce_context = load_context()
@@ -128,9 +140,19 @@ class TrackCommand(Command):
128
140
 
129
141
  if recce_context is not None:
130
142
  if recce_context.adapter_type == "dbt":
131
- props['adapter_type'] = 'DBT'
143
+ props["adapter_type"] = "DBT"
144
+ # Add dbt warehouse type only for dbt adapter
145
+ try:
146
+ from recce.adapter.dbt_adapter import DbtAdapter
147
+
148
+ dbt_adapter: DbtAdapter = recce_context.adapter
149
+ warehouse_type = dbt_adapter.adapter.type()
150
+ props["warehouse_type"] = warehouse_type
151
+ except Exception:
152
+ # If we can't get the warehouse type, skip it
153
+ pass
132
154
  elif recce_context.adapter_type == "sqlmesh":
133
- props['adapter_type'] = 'SQLMesh'
155
+ props["adapter_type"] = "SQLMesh"
134
156
 
135
- event.log_event(props, 'command', params=ctx.params)
157
+ event.log_event(props, "command", params=ctx.params)
136
158
  event.flush_events()
recce/exceptions.py CHANGED
@@ -7,7 +7,7 @@ class RecceException(Exception):
7
7
 
8
8
  class RecceCancelException(RecceException):
9
9
  def __init__(self):
10
- super().__init__('Cancelled', False)
10
+ super().__init__("Cancelled", False)
11
11
 
12
12
 
13
13
  class RecceConfigException(RecceException):
recce/git.py CHANGED
@@ -1,12 +1,12 @@
1
1
  import os
2
2
 
3
- from git import Repo, InvalidGitRepositoryError
3
+ from git import InvalidGitRepositoryError, Repo
4
4
 
5
5
 
6
6
  def current_default_branch():
7
7
  try:
8
8
  repo = Repo(search_parent_directories=True)
9
- return repo.remotes.origin.refs['HEAD'].reference.remote_head
9
+ return repo.remotes.origin.refs["HEAD"].reference.remote_head
10
10
  except Exception:
11
11
  return None
12
12
 
@@ -53,19 +53,19 @@ def commit_hash_from_branch(branch: str, short_length: int = 7, short: bool = Fa
53
53
  return None
54
54
 
55
55
 
56
- def hosting_repo(remote: str = 'origin'):
56
+ def hosting_repo(remote: str = "origin"):
57
57
  try:
58
58
  repo = Repo(search_parent_directories=True)
59
59
  origin_url = repo.remote(name=remote).url
60
60
  remote_repo = None
61
61
 
62
- if origin_url.startswith('git@'):
62
+ if origin_url.startswith("git@"):
63
63
  # Handle git@github.com:user/repo.git
64
- remote_repo = origin_url.split(':')[1].replace('.git', '')
64
+ remote_repo = origin_url.split(":")[1].replace(".git", "")
65
65
 
66
- elif origin_url.startswith('https://') or origin_url.startswith('http://'):
66
+ elif origin_url.startswith("https://") or origin_url.startswith("http://"):
67
67
  # Handle https://github.com/user/repo.git or http://github.com/user/repo.git
68
- remote_repo = '/'.join(origin_url.split('/')[-2:]).replace('.git', '')
68
+ remote_repo = "/".join(origin_url.split("/")[-2:]).replace(".git", "")
69
69
 
70
70
  return remote_repo
71
71
  except ValueError:
recce/github.py CHANGED
@@ -3,10 +3,10 @@ import os
3
3
  import re
4
4
  import zipfile
5
5
  from datetime import datetime
6
- from typing import List, Tuple, Optional
6
+ from typing import List, Optional, Tuple
7
7
 
8
8
  import requests
9
- from github import Artifact, Github, Auth, UnknownObjectException, PullRequest
9
+ from github import Artifact, Auth, Github, PullRequest, UnknownObjectException
10
10
 
11
11
  from recce.git import current_branch, hosting_repo
12
12
 
@@ -16,7 +16,7 @@ def download_artifact(github_token: str, artifact: Artifact) -> List[str]:
16
16
  Download the artifact from the Github.
17
17
  """
18
18
  headers = {
19
- 'Authorization': f'Bearer {github_token}',
19
+ "Authorization": f"Bearer {github_token}",
20
20
  }
21
21
  r = requests.get(artifact.archive_download_url, headers=headers)
22
22
  r.raise_for_status()
@@ -27,17 +27,17 @@ def download_artifact(github_token: str, artifact: Artifact) -> List[str]:
27
27
 
28
28
  def recce_ci_artifact(**kwargs):
29
29
  """
30
- Download the artifact from the GitHub CI.
30
+ Download the artifact from the GitHub CI.
31
31
  """
32
- from rich.console import Console
33
-
34
32
  import git
33
+
35
34
  # Authentication is defined via github.Auth
36
35
  from github import Auth, Github
36
+ from rich.console import Console
37
37
 
38
38
  console = Console()
39
- github_token = kwargs.get('github_token')
40
- github_repository = kwargs.get('github_repository')
39
+ github_token = kwargs.get("github_token")
40
+ github_repository = kwargs.get("github_repository")
41
41
 
42
42
  if github_token is None:
43
43
  console.print("[[red]Error[/red]] Missing GitHub token. Please provide a GitHub token.")
@@ -70,11 +70,11 @@ def recce_ci_artifact(**kwargs):
70
70
 
71
71
  head_branch = github_repo.get_branch(current_branch)
72
72
 
73
- console.rule('GitHub Repository Information')
74
- console.print(f'Repository: {github_repository}')
75
- console.print(f'Current Branch: {current_branch}')
73
+ console.rule("GitHub Repository Information")
74
+ console.print(f"Repository: {github_repository}")
75
+ console.print(f"Current Branch: {current_branch}")
76
76
 
77
- pull_requests = github_repo.get_pulls(head=f'{github_owner}:{current_branch}')
77
+ pull_requests = github_repo.get_pulls(head=f"{github_owner}:{current_branch}")
78
78
  if pull_requests.totalCount == 0:
79
79
  # No pull request found for the current branch
80
80
  console.print("[[red]Error[/red]] No pull request found for the current branch.")
@@ -82,32 +82,32 @@ def recce_ci_artifact(**kwargs):
82
82
 
83
83
  pr = pull_requests[0]
84
84
 
85
- console.rule('GitHub Pull Request Information')
86
- console.print(f'{pr.title} - {pr.html_url}')
87
- console.print(f'State: {pr.state.title()}')
88
- console.print(f'Author: {pr.user.name}')
89
- console.print(f'Created At: {pr.created_at}')
85
+ console.rule("GitHub Pull Request Information")
86
+ console.print(f"{pr.title} - {pr.html_url}")
87
+ console.print(f"State: {pr.state.title()}")
88
+ console.print(f"Author: {pr.user.name}")
89
+ console.print(f"Created At: {pr.created_at}")
90
90
 
91
91
  workflow_runs = github_repo.get_workflow_runs(
92
- event='pull_request',
93
- status='success',
92
+ event="pull_request",
93
+ status="success",
94
94
  branch=head_branch,
95
95
  )
96
96
  if workflow_runs.totalCount == 0:
97
97
  console.print("[[yellow]Skip[/yellow]] No successful workflow runs found.")
98
98
  return 0
99
99
  last_workflow_run = workflow_runs[0]
100
- console.rule('GitHub Workflow Run Information')
101
- console.print(f'Last Workflow Run: {last_workflow_run.name} {last_workflow_run.html_url}')
100
+ console.rule("GitHub Workflow Run Information")
101
+ console.print(f"Last Workflow Run: {last_workflow_run.name} {last_workflow_run.html_url}")
102
102
  artifacts = last_workflow_run.get_artifacts()
103
103
  if artifacts.totalCount == 0:
104
104
  console.print("[[yellow]Skip[/yellow]] No artifacts found.")
105
105
  return 0
106
106
  for artifact in artifacts:
107
- console.print(f'Artifact: {artifact.name} {artifact.archive_download_url}')
107
+ console.print(f"Artifact: {artifact.name} {artifact.archive_download_url}")
108
108
  artifact_files = download_artifact(github_token, artifact)
109
109
  console.print(f'Extracted Files: {", ".join(artifact_files)}')
110
- console.rule('Complete')
110
+ console.rule("Complete")
111
111
  return 0
112
112
 
113
113
 
@@ -121,7 +121,7 @@ def get_pull_request(branch, owner, repo_name, github_token=None) -> Tuple[Optio
121
121
 
122
122
  try:
123
123
  repo = g.get_repo(f"{owner}/{repo_name}")
124
- pulls = repo.get_pulls(state='open')
124
+ pulls = repo.get_pulls(state="open")
125
125
 
126
126
  for pr in pulls:
127
127
  if pr.head.ref == branch:
@@ -129,7 +129,10 @@ def get_pull_request(branch, owner, repo_name, github_token=None) -> Tuple[Optio
129
129
 
130
130
  except UnknownObjectException:
131
131
  if github_token is not None:
132
- return None, f"Repository {owner}/{repo_name} not found. If it is private repo, please add the 'repo' scope to the token."
132
+ return (
133
+ None,
134
+ f"Repository {owner}/{repo_name} not found. If it is private repo, please add the 'repo' scope to the token.",
135
+ )
133
136
 
134
137
  return None, None
135
138
 
@@ -139,53 +142,54 @@ def recce_pr_information(github_token=None) -> Tuple[Optional[type(PullRequest)]
139
142
  repo = hosting_repo()
140
143
 
141
144
  if not repo:
142
- return None, 'This is not a git repository.'
143
- if '/' not in repo:
144
- return None, 'This is not a GitHub repository.'
145
+ return None, "This is not a git repository."
146
+ if "/" not in repo:
147
+ return None, "This is not a GitHub repository."
145
148
 
146
- owner, repo_name = repo.split('/')
149
+ owner, repo_name = repo.split("/")
147
150
 
148
151
  github_token = github_token if github_token else os.getenv("GITHUB_TOKEN")
149
152
  return get_pull_request(branch, owner, repo_name, github_token)
150
153
 
151
154
 
152
155
  def is_github_codespace():
153
- return os.getenv('CODESPACES') == 'true'
156
+ return os.getenv("CODESPACES") == "true"
154
157
 
155
158
 
156
159
  def get_github_codespace_name():
157
- return os.getenv('CODESPACE_NAME')
160
+ return os.getenv("CODESPACE_NAME")
158
161
 
159
162
 
160
163
  def get_github_codespace_info():
161
164
  if is_github_codespace() is False:
162
165
  return None
163
166
 
164
- codespace_name = os.environ.get('CODESPACE_NAME')
165
- github_token = os.environ.get('GITHUB_TOKEN')
167
+ codespace_name = os.environ.get("CODESPACE_NAME")
168
+ github_token = os.environ.get("GITHUB_TOKEN")
166
169
 
167
170
  response = requests.get(
168
- f'https://api.github.com/user/codespaces/{codespace_name}',
171
+ f"https://api.github.com/user/codespaces/{codespace_name}",
169
172
  headers={
170
- 'Accept': 'application/vnd.github+json',
171
- 'Authorization': f'token {github_token}',
172
- 'X-GitHub-Api-Version': '2022-11-28',
173
- })
173
+ "Accept": "application/vnd.github+json",
174
+ "Authorization": f"token {github_token}",
175
+ "X-GitHub-Api-Version": "2022-11-28",
176
+ },
177
+ )
174
178
 
175
179
  if response.status_code != 200:
176
180
  return None
177
181
  codespace_info = response.json()
178
182
 
179
183
  return dict(
180
- name=codespace_info.get('name'),
181
- machine=codespace_info.get('machine'),
182
- prebuild=codespace_info.get('prebuild'),
183
- created_at=codespace_info.get('created_at'),
184
- updated_at=codespace_info.get('updated_at'),
185
- last_used_at=codespace_info.get('last_used_at'),
186
- state=codespace_info.get('state'),
187
- location=codespace_info.get('location'),
188
- idle_timeout_minutes=codespace_info.get('idle_timeout_minutes'),
184
+ name=codespace_info.get("name"),
185
+ machine=codespace_info.get("machine"),
186
+ prebuild=codespace_info.get("prebuild"),
187
+ created_at=codespace_info.get("created_at"),
188
+ updated_at=codespace_info.get("updated_at"),
189
+ last_used_at=codespace_info.get("last_used_at"),
190
+ state=codespace_info.get("state"),
191
+ location=codespace_info.get("location"),
192
+ idle_timeout_minutes=codespace_info.get("idle_timeout_minutes"),
189
193
  )
190
194
 
191
195
 
@@ -194,25 +198,25 @@ def get_github_codespace_available_at(codespace):
194
198
  return None
195
199
 
196
200
  def search_in_file(file_path, search_string):
197
- with open(file_path, 'r') as f:
201
+ with open(file_path, "r", encoding="utf-8") as f:
198
202
  for _, line in enumerate(f, 1):
199
203
  if search_string in line:
200
204
  return line
201
205
  return None
202
206
 
203
207
  def extract_datatime(log_line):
204
- pattern = r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}).*\]'
208
+ pattern = r"\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}).*\]"
205
209
  match = re.search(pattern, log_line)
206
210
  if match:
207
211
  datetime_str = match.group(1)
208
- return datetime.strptime(datetime_str, '%Y-%m-%d %H:%M:%S.%f')
212
+ return datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S.%f")
209
213
  return None
210
214
 
211
- github_codepsce_log_dir = '/tmp/codespaces_logs'
215
+ github_codepsce_log_dir = "/tmp/codespaces_logs"
212
216
  try:
213
217
  log_file = os.listdir(github_codepsce_log_dir)[-1] # Get the latest log file
214
- start_monitor_line = search_in_file(f'{github_codepsce_log_dir}/{log_file}', 'Starting monitor')
218
+ start_monitor_line = search_in_file(f"{github_codepsce_log_dir}/{log_file}", "Starting monitor")
215
219
  return extract_datatime(start_monitor_line)
216
220
  except Exception:
217
221
  # If there is any error, use the updated_at time from the codespace info
218
- return datetime.fromisoformat(codespace.get('updated_at'))
222
+ return datetime.fromisoformat(codespace.get("updated_at"))