recce-nightly 0.62.0.20250417__py3-none-any.whl → 1.30.0.20251221__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (245) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +27 -22
  3. recce/adapter/base.py +11 -14
  4. recce/adapter/dbt_adapter/__init__.py +845 -461
  5. recce/adapter/dbt_adapter/dbt_version.py +3 -0
  6. recce/adapter/sqlmesh_adapter.py +24 -35
  7. recce/apis/check_api.py +59 -42
  8. recce/apis/check_events_api.py +353 -0
  9. recce/apis/check_func.py +41 -35
  10. recce/apis/run_api.py +25 -19
  11. recce/apis/run_func.py +64 -25
  12. recce/artifact.py +119 -51
  13. recce/cli.py +1301 -324
  14. recce/config.py +43 -34
  15. recce/connect_to_cloud.py +138 -0
  16. recce/core.py +55 -47
  17. recce/data/404/index.html +2 -0
  18. recce/data/404.html +2 -1
  19. recce/data/__next.@lineage.!KHNsb3Qp.__PAGE__.txt +7 -0
  20. recce/data/__next.@lineage.!KHNsb3Qp.txt +4 -0
  21. recce/data/__next.__PAGE__.txt +6 -0
  22. recce/data/__next._full.txt +32 -0
  23. recce/data/__next._head.txt +8 -0
  24. recce/data/__next._index.txt +14 -0
  25. recce/data/__next._tree.txt +8 -0
  26. recce/data/_next/static/chunks/025a7e3e3f9f40ae.js +1 -0
  27. recce/data/_next/static/chunks/0ce56d67ef5779ca.js +4 -0
  28. recce/data/_next/static/chunks/1a6a78780155dac7.js +48 -0
  29. recce/data/_next/static/chunks/1de8485918b9182a.css +2 -0
  30. recce/data/_next/static/chunks/1e4b1b50d1e34993.js +1 -0
  31. recce/data/_next/static/chunks/206d5d181e4c738e.js +1 -0
  32. recce/data/_next/static/chunks/2c357efc34c5b859.js +25 -0
  33. recce/data/_next/static/chunks/2e9d95d2d48c479c.js +1 -0
  34. recce/data/_next/static/chunks/2f016dc4a3edad2e.js +2 -0
  35. recce/data/_next/static/chunks/313251962d698f7c.js +1 -0
  36. recce/data/_next/static/chunks/3a9f021f38eb5574.css +1 -0
  37. recce/data/_next/static/chunks/40079da8d2b8f651.js +1 -0
  38. recce/data/_next/static/chunks/4599182bffb64661.js +38 -0
  39. recce/data/_next/static/chunks/4e62f6e184173580.js +1 -0
  40. recce/data/_next/static/chunks/5c4dfb0d09eaa401.js +1 -0
  41. recce/data/_next/static/chunks/69e4f06ccfdfc3ac.js +1 -0
  42. recce/data/_next/static/chunks/6b206cb4707d6bee.js +1 -0
  43. recce/data/_next/static/chunks/6d8557f062aa4386.css +1 -0
  44. recce/data/_next/static/chunks/7fbe3650bd83b6b5.js +1 -0
  45. recce/data/_next/static/chunks/83fa823a825674f6.js +1 -0
  46. recce/data/_next/static/chunks/848a6c9b5f55f7ed.js +1 -0
  47. recce/data/_next/static/chunks/859462b0858aef88.css +2 -0
  48. recce/data/_next/static/chunks/923964f18c87d0f1.css +1 -0
  49. recce/data/_next/static/chunks/939390f911895d7c.js +48 -0
  50. recce/data/_next/static/chunks/99a9817237a07f43.js +1 -0
  51. recce/data/_next/static/chunks/9fed8b4b2b924054.js +5 -0
  52. recce/data/_next/static/chunks/b6949f6c5892110c.js +1 -0
  53. recce/data/_next/static/chunks/b851a1d3f8149828.js +1 -0
  54. recce/data/_next/static/chunks/c734f9ad957de0b4.js +1 -0
  55. recce/data/_next/static/chunks/cdde321b0ec75717.js +2 -0
  56. recce/data/_next/static/chunks/d0f91117d77ff844.css +1 -0
  57. recce/data/_next/static/chunks/d6c8667911c2500f.js +1 -0
  58. recce/data/_next/static/chunks/da8dab68c02752cf.js +74 -0
  59. recce/data/_next/static/chunks/dc074049c9d12d97.js +109 -0
  60. recce/data/_next/static/chunks/ee7f1a8227342421.js +1 -0
  61. recce/data/_next/static/chunks/fa2f4e56c2fccc73.js +1 -0
  62. recce/data/_next/static/chunks/turbopack-1fad664f62979b93.js +3 -0
  63. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  64. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  65. recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
  66. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  67. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
  68. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  69. recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
  70. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  71. recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
  72. recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
  73. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  74. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_buildManifest.js +11 -0
  75. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_clientMiddlewareManifest.json +1 -0
  76. recce/data/_not-found/__next._full.txt +24 -0
  77. recce/data/_not-found/__next._head.txt +8 -0
  78. recce/data/_not-found/__next._index.txt +13 -0
  79. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  80. recce/data/_not-found/__next._not-found.txt +4 -0
  81. recce/data/_not-found/__next._tree.txt +6 -0
  82. recce/data/_not-found/index.html +2 -0
  83. recce/data/_not-found/index.txt +24 -0
  84. recce/data/auth_callback.html +68 -0
  85. recce/data/checks/__next.@lineage.__DEFAULT__.txt +7 -0
  86. recce/data/checks/__next._full.txt +39 -0
  87. recce/data/checks/__next._head.txt +8 -0
  88. recce/data/checks/__next._index.txt +14 -0
  89. recce/data/checks/__next._tree.txt +8 -0
  90. recce/data/checks/__next.checks.__PAGE__.txt +10 -0
  91. recce/data/checks/__next.checks.txt +4 -0
  92. recce/data/checks/index.html +2 -0
  93. recce/data/checks/index.txt +39 -0
  94. recce/data/imgs/reload-image.svg +4 -0
  95. recce/data/index.html +2 -27
  96. recce/data/index.txt +32 -7
  97. recce/data/lineage/__next.@lineage.__DEFAULT__.txt +7 -0
  98. recce/data/lineage/__next._full.txt +39 -0
  99. recce/data/lineage/__next._head.txt +8 -0
  100. recce/data/lineage/__next._index.txt +14 -0
  101. recce/data/lineage/__next._tree.txt +8 -0
  102. recce/data/lineage/__next.lineage.__PAGE__.txt +10 -0
  103. recce/data/lineage/__next.lineage.txt +4 -0
  104. recce/data/lineage/index.html +2 -0
  105. recce/data/lineage/index.txt +39 -0
  106. recce/data/query/__next.@lineage.__DEFAULT__.txt +7 -0
  107. recce/data/query/__next._full.txt +37 -0
  108. recce/data/query/__next._head.txt +8 -0
  109. recce/data/query/__next._index.txt +14 -0
  110. recce/data/query/__next._tree.txt +8 -0
  111. recce/data/query/__next.query.__PAGE__.txt +9 -0
  112. recce/data/query/__next.query.txt +4 -0
  113. recce/data/query/index.html +2 -0
  114. recce/data/query/index.txt +37 -0
  115. recce/diff.py +6 -12
  116. recce/event/CONFIG.bak +1 -0
  117. recce/event/__init__.py +86 -74
  118. recce/event/collector.py +33 -22
  119. recce/event/track.py +49 -27
  120. recce/exceptions.py +1 -1
  121. recce/git.py +7 -7
  122. recce/github.py +57 -53
  123. recce/mcp_server.py +725 -0
  124. recce/models/__init__.py +4 -1
  125. recce/models/check.py +438 -21
  126. recce/models/run.py +1 -0
  127. recce/models/types.py +134 -28
  128. recce/pull_request.py +27 -25
  129. recce/run.py +179 -122
  130. recce/server.py +394 -104
  131. recce/state/__init__.py +31 -0
  132. recce/state/cloud.py +644 -0
  133. recce/state/const.py +26 -0
  134. recce/state/local.py +56 -0
  135. recce/state/state.py +119 -0
  136. recce/state/state_loader.py +174 -0
  137. recce/summary.py +196 -149
  138. recce/tasks/__init__.py +19 -3
  139. recce/tasks/core.py +11 -13
  140. recce/tasks/dataframe.py +82 -18
  141. recce/tasks/histogram.py +69 -34
  142. recce/tasks/lineage.py +2 -2
  143. recce/tasks/profile.py +152 -86
  144. recce/tasks/query.py +180 -89
  145. recce/tasks/rowcount.py +37 -31
  146. recce/tasks/schema.py +18 -15
  147. recce/tasks/top_k.py +35 -35
  148. recce/tasks/utils.py +147 -0
  149. recce/tasks/valuediff.py +247 -155
  150. recce/util/__init__.py +3 -0
  151. recce/util/api_token.py +80 -0
  152. recce/util/breaking.py +105 -100
  153. recce/util/cll.py +274 -219
  154. recce/util/cloud/__init__.py +15 -0
  155. recce/util/cloud/base.py +115 -0
  156. recce/util/cloud/check_events.py +190 -0
  157. recce/util/cloud/checks.py +242 -0
  158. recce/util/io.py +22 -17
  159. recce/util/lineage.py +65 -16
  160. recce/util/logger.py +1 -1
  161. recce/util/onboarding_state.py +45 -0
  162. recce/util/perf_tracking.py +85 -0
  163. recce/util/recce_cloud.py +347 -72
  164. recce/util/singleton.py +4 -4
  165. recce/util/startup_perf.py +121 -0
  166. recce/yaml/__init__.py +7 -10
  167. recce_nightly-1.30.0.20251221.dist-info/METADATA +195 -0
  168. recce_nightly-1.30.0.20251221.dist-info/RECORD +183 -0
  169. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/WHEEL +1 -2
  170. recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
  171. recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
  172. recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
  173. recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
  174. recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
  175. recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
  176. recce/data/_next/static/chunks/500-e51c92a025a51234.js +0 -65
  177. recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
  178. recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
  179. recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
  180. recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
  181. recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
  182. recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
  183. recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
  184. recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
  185. recce/data/_next/static/chunks/app/page-9adc25782272ed2e.js +0 -1
  186. recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
  187. recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
  188. recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
  189. recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
  190. recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
  191. recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
  192. recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
  193. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  194. recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
  195. recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
  196. recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
  197. recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
  198. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  199. recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
  200. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  201. recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
  202. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
  203. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  204. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  205. recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
  206. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  207. recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
  208. recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
  209. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  210. recce/data/_next/static/qiyFlux77VkhxiceAJe_F/_buildManifest.js +0 -1
  211. recce/state.py +0 -753
  212. recce_nightly-0.62.0.20250417.dist-info/METADATA +0 -311
  213. recce_nightly-0.62.0.20250417.dist-info/RECORD +0 -139
  214. recce_nightly-0.62.0.20250417.dist-info/top_level.txt +0 -2
  215. tests/__init__.py +0 -0
  216. tests/adapter/__init__.py +0 -0
  217. tests/adapter/dbt_adapter/__init__.py +0 -0
  218. tests/adapter/dbt_adapter/conftest.py +0 -13
  219. tests/adapter/dbt_adapter/dbt_test_helper.py +0 -283
  220. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -40
  221. tests/adapter/dbt_adapter/test_dbt_cll.py +0 -102
  222. tests/adapter/dbt_adapter/test_selector.py +0 -177
  223. tests/tasks/__init__.py +0 -0
  224. tests/tasks/conftest.py +0 -4
  225. tests/tasks/test_histogram.py +0 -137
  226. tests/tasks/test_lineage.py +0 -42
  227. tests/tasks/test_preset_checks.py +0 -50
  228. tests/tasks/test_profile.py +0 -73
  229. tests/tasks/test_query.py +0 -151
  230. tests/tasks/test_row_count.py +0 -116
  231. tests/tasks/test_schema.py +0 -99
  232. tests/tasks/test_top_k.py +0 -73
  233. tests/tasks/test_valuediff.py +0 -74
  234. tests/test_cli.py +0 -122
  235. tests/test_config.py +0 -45
  236. tests/test_core.py +0 -27
  237. tests/test_dbt.py +0 -36
  238. tests/test_pull_request.py +0 -130
  239. tests/test_server.py +0 -98
  240. tests/test_state.py +0 -123
  241. tests/test_summary.py +0 -57
  242. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  243. /recce/data/_next/static/{qiyFlux77VkhxiceAJe_F → nX-Uz0AH6Tc6hIQUFGqaB}/_ssgManifest.js +0 -0
  244. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/entry_points.txt +0 -0
  245. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,85 @@
1
+ import time
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class LineagePerfTracker:
7
+ lineage_start = None
8
+ lineage_elapsed = None
9
+ column_lineage_start = None
10
+ column_lineage_elapsed = None
11
+
12
+ total_nodes = None
13
+ init_nodes = None
14
+ cll_nodes = 0
15
+ change_analysis_nodes = 0
16
+ anchor_nodes = None
17
+
18
+ params = None
19
+
20
+ def start_lineage(self):
21
+ self.lineage_start = time.perf_counter_ns()
22
+
23
+ def end_lineage(self):
24
+ if self.lineage_start is None:
25
+ return
26
+ self.lineage_elapsed = (time.perf_counter_ns() - self.lineage_start) / 1000000
27
+
28
+ def start_column_lineage(self):
29
+ self.column_lineage_start = time.perf_counter_ns()
30
+
31
+ def end_column_lineage(self):
32
+ if self.column_lineage_start is None:
33
+ return
34
+ self.column_lineage_elapsed = (time.perf_counter_ns() - self.column_lineage_start) / 1000000
35
+
36
+ def set_total_nodes(self, total_nodes):
37
+ self.total_nodes = total_nodes
38
+
39
+ def set_init_nodes(self, init_nodes):
40
+ self.init_nodes = init_nodes
41
+
42
+ def set_anchor_nodes(self, anchor_nodes):
43
+ self.anchor_nodes = anchor_nodes
44
+
45
+ def increment_cll_nodes(self):
46
+ self.cll_nodes += 1
47
+
48
+ def increment_change_analysis_nodes(self):
49
+ self.change_analysis_nodes += 1
50
+
51
+ def set_params(self, has_node, has_column, change_analysis, no_cll, no_upstream, no_downstream):
52
+ self.params = {
53
+ "has_node": has_node,
54
+ "has_column": has_column,
55
+ "change_analysis": change_analysis,
56
+ "no_cll": no_cll,
57
+ "no_upstream": no_upstream,
58
+ "no_downstream": no_downstream,
59
+ }
60
+
61
+ def to_dict(self):
62
+ return {
63
+ "lineage_elapsed_ms": self.lineage_elapsed,
64
+ "column_lineage_elapsed_ms": self.column_lineage_elapsed,
65
+ "total_nodes": self.total_nodes,
66
+ "init_nodes": self.init_nodes,
67
+ "cll_nodes": self.cll_nodes,
68
+ "change_analysis_nodes": self.change_analysis_nodes,
69
+ "anchor_nodes": self.anchor_nodes,
70
+ "params": self.params,
71
+ }
72
+
73
+ def reset(self):
74
+ self.lineage_start = None
75
+ self.lineage_elapsed = None
76
+ self.column_lineage_start = None
77
+ self.column_lineage_elapsed = None
78
+
79
+ self.total_nodes = None
80
+ self.init_nodes = None
81
+ self.change_analysis_nodes = 0
82
+ self.cll_nodes = 0
83
+ self.anchor_nodes = 0
84
+
85
+ self.params = None
recce/util/recce_cloud.py CHANGED
@@ -1,20 +1,30 @@
1
1
  import json
2
2
  import logging
3
3
  import os
4
- from typing import Dict, IO
4
+ import typing
5
+ from typing import IO, Dict
5
6
 
6
7
  import requests
7
8
 
9
+ from recce import get_version
10
+ from recce.event import get_user_id, is_anonymous_tracking
8
11
  from recce.pull_request import PullRequestInfo
9
12
 
10
- RECCE_CLOUD_API_HOST = os.environ.get('RECCE_CLOUD_API_HOST', 'https://cloud.datarecce.io')
13
+ if typing.TYPE_CHECKING:
14
+ from recce.util.cloud import ChecksCloud
11
15
 
12
- logger = logging.getLogger('uvicorn')
16
+ RECCE_CLOUD_API_HOST = os.environ.get("RECCE_CLOUD_API_HOST", "https://cloud.datarecce.io")
17
+ RECCE_CLOUD_BASE_URL = os.environ.get("RECCE_CLOUD_BASE_URL", RECCE_CLOUD_API_HOST)
18
+
19
+ DOCKER_INTERNAL_URL_PREFIX = "http://host.docker.internal"
20
+ LOCALHOST_URL_PREFIX = "http://localhost"
21
+
22
+ logger = logging.getLogger("uvicorn")
13
23
 
14
24
 
15
25
  class PresignedUrlMethod:
16
- UPLOAD = 'upload'
17
- DOWNLOAD = 'download'
26
+ UPLOAD = "upload"
27
+ DOWNLOAD = "download"
18
28
 
19
29
 
20
30
  class RecceCloudException(Exception):
@@ -23,7 +33,7 @@ class RecceCloudException(Exception):
23
33
  self.status_code = status_code
24
34
 
25
35
  try:
26
- reason = json.loads(reason).get('detail', '')
36
+ reason = json.loads(reason).get("detail", "")
27
37
  except json.JSONDecodeError:
28
38
  pass
29
39
  self.reason = reason
@@ -31,147 +41,412 @@ class RecceCloudException(Exception):
31
41
 
32
42
  class RecceCloud:
33
43
  def __init__(self, token: str):
44
+ if token is None:
45
+ raise ValueError("Token cannot be None.")
34
46
  self.token = token
35
- self.base_url = f'{RECCE_CLOUD_API_HOST}/api/v1'
47
+ self.token_type = "github_token" if token.startswith(("ghp_", "gho_", "ghu_", "ghs_", "ghr_")) else "api_token"
48
+ self.base_url = f"{RECCE_CLOUD_API_HOST}/api/v1"
49
+ self.base_url_v2 = f"{RECCE_CLOUD_API_HOST}/api/v2"
50
+
51
+ # Initialize modular clients
52
+ self._checks_client = None
53
+
54
+ @property
55
+ def checks(self) -> "ChecksCloud":
56
+ """
57
+ Get the checks client for check operations.
58
+
59
+ Returns:
60
+ ChecksCloud instance for check operations
61
+
62
+ Example:
63
+ >>> cloud = RecceCloud(token="your-token")
64
+ >>> checks = cloud.checks.list_checks("org", "proj", "sess")
65
+ """
66
+ if self._checks_client is None:
67
+ from recce.util.cloud import ChecksCloud
36
68
 
37
- def _request(self, method, url, **kwargs):
69
+ self._checks_client = ChecksCloud(self.token)
70
+ return self._checks_client
71
+
72
+ def _request(self, method, url, headers: Dict = None, **kwargs):
38
73
  headers = {
39
- 'Authorization': f'Bearer {self.token}'
74
+ **(headers or {}),
75
+ "Authorization": f"Bearer {self.token}",
40
76
  }
41
77
  return requests.request(method, url, headers=headers, **kwargs)
42
78
 
43
- def get_presigned_url(self,
44
- method: PresignedUrlMethod,
45
- repository: str,
46
- artifact_name: str,
47
- metadata: dict = None,
48
- pr_id: int = None,
49
- branch: str = None) -> str:
79
+ def verify_token(self) -> bool:
80
+ if self.token_type == "github_token":
81
+ return True
82
+ # Verify the Recce Cloud API token
83
+ api_url = f"{self.base_url}/verify-token"
84
+ try:
85
+ headers: Dict = None
86
+ if is_anonymous_tracking():
87
+ headers = {
88
+ "X-Recce-Oss-User-Id": get_user_id(),
89
+ "X-Recce-Oss-Version": get_version(),
90
+ }
91
+ response = self._request("GET", api_url, headers=headers)
92
+ if response.status_code == 200:
93
+ return True
94
+ except Exception:
95
+ pass
96
+ return False
97
+
98
+ def get_presigned_url_by_github_repo(
99
+ self,
100
+ method: PresignedUrlMethod,
101
+ repository: str,
102
+ artifact_name: str,
103
+ metadata: dict = None,
104
+ pr_id: int = None,
105
+ branch: str = None,
106
+ ) -> str:
50
107
  response = self._fetch_presigned_url(method, repository, artifact_name, metadata, pr_id, branch)
51
- return response.get('presigned_url')
108
+ return response.get("presigned_url")
52
109
 
53
- def get_download_presigned_url_with_tags(self,
54
- repository: str,
55
- artifact_name: str,
56
- branch: str = None) -> (str, dict):
110
+ def _replace_localhost_with_docker_internal(self, url: str) -> str:
111
+ if url is None:
112
+ return None
113
+ if (
114
+ os.environ.get("RECCE_SHARE_INSTANCE_ENV") == "docker"
115
+ or os.environ.get("RECCE_TASK_INSTANCE_ENV") == "docker"
116
+ or os.environ.get("RECCE_INSTANCE_ENV") == "docker"
117
+ ):
118
+ # For local development, convert the presigned URL from localhost to host.docker.internal
119
+ if url.startswith(LOCALHOST_URL_PREFIX):
120
+ return url.replace(LOCALHOST_URL_PREFIX, DOCKER_INTERNAL_URL_PREFIX)
121
+ return url
122
+
123
+ def get_presigned_url_by_share_id(
124
+ self,
125
+ method: PresignedUrlMethod,
126
+ share_id: str,
127
+ metadata: dict = None,
128
+ ) -> str:
129
+ response = self._fetch_presigned_url_by_share_id(method, share_id, metadata=metadata)
130
+ presigned_url = response.get("presigned_url")
131
+ if not presigned_url:
132
+ raise RecceCloudException(
133
+ message="Failed to get presigned URL from Recce Cloud.",
134
+ reason="No presigned URL returned from the server.",
135
+ status_code=404,
136
+ )
137
+ presigned_url = self._replace_localhost_with_docker_internal(presigned_url)
138
+ return presigned_url
139
+
140
+ def get_download_presigned_url_by_github_repo_with_tags(
141
+ self, repository: str, artifact_name: str, branch: str = None
142
+ ) -> (str, dict):
57
143
  response = self._fetch_presigned_url(PresignedUrlMethod.DOWNLOAD, repository, artifact_name, branch=branch)
58
- return response.get('presigned_url'), response.get('tags', {})
59
-
60
- def _fetch_presigned_url(self,
61
- method: PresignedUrlMethod,
62
- repository: str,
63
- artifact_name: str,
64
- metadata: dict = None,
65
- pr_id: int = None,
66
- branch: str = None) -> str:
144
+ return response.get("presigned_url"), response.get("tags", {})
145
+
146
+ def _fetch_presigned_url(
147
+ self,
148
+ method: PresignedUrlMethod,
149
+ repository: str,
150
+ artifact_name: str,
151
+ metadata: dict = None,
152
+ pr_id: int = None,
153
+ branch: str = None,
154
+ ) -> str:
67
155
  if pr_id is not None:
68
- api_url = f'{self.base_url}/{repository}/pulls/{pr_id}/artifacts/{method}?artifact_name={artifact_name}&enable_ssec=true'
156
+ api_url = f"{self.base_url}/{repository}/pulls/{pr_id}/artifacts/{method}?artifact_name={artifact_name}&enable_ssec=true"
69
157
  elif branch is not None:
70
- api_url = f'{self.base_url}/{repository}/commits/{branch}/artifacts/{method}?artifact_name={artifact_name}&enable_ssec=true'
158
+ api_url = f"{self.base_url}/{repository}/commits/{branch}/artifacts/{method}?artifact_name={artifact_name}&enable_ssec=true"
71
159
  else:
72
- raise ValueError('Either pr_id or sha must be provided.')
73
- response = self._request('POST', api_url, json=metadata)
160
+ raise ValueError("Either pr_id or sha must be provided.")
161
+ response = self._request("POST", api_url, json=metadata)
74
162
  if response.status_code != 200:
75
163
  raise RecceCloudException(
76
- message='Failed to {method} artifact {preposition} Recce Cloud.'.format(
77
- method=method,
78
- preposition='from' if method == PresignedUrlMethod.DOWNLOAD else 'to'
164
+ message="Failed to {method} artifact {preposition} Recce Cloud.".format(
165
+ method=method, preposition="from" if method == PresignedUrlMethod.DOWNLOAD else "to"
79
166
  ),
80
167
  reason=response.text,
81
- status_code=response.status_code
168
+ status_code=response.status_code,
169
+ )
170
+ return response.json()
171
+
172
+ def _fetch_presigned_url_by_share_id(
173
+ self,
174
+ method: PresignedUrlMethod,
175
+ share_id: str,
176
+ metadata: dict = None,
177
+ ):
178
+ api_url = f"{self.base_url}/shares/{share_id}/presigned/{method}"
179
+ data = None
180
+ # Only provide metadata for upload requests
181
+ if method == PresignedUrlMethod.UPLOAD:
182
+ # Covert metadata values to strings to ensure JSON serializability
183
+ data = {"metadata": {key: str(value) for key, value in metadata.items()}} if metadata else None
184
+ response = self._request(
185
+ "POST",
186
+ api_url,
187
+ json=data,
188
+ )
189
+ if response.status_code != 200:
190
+ raise RecceCloudException(
191
+ message="Failed to {method} artifact {preposition} Recce Cloud.".format(
192
+ method=method, preposition="from" if method == PresignedUrlMethod.DOWNLOAD else "to"
193
+ ),
194
+ reason=response.text,
195
+ status_code=response.status_code,
82
196
  )
83
197
  return response.json()
84
198
 
85
199
  def get_artifact_metadata(self, pr_info: PullRequestInfo) -> dict:
86
- api_url = f'{self.base_url}/{pr_info.repository}/pulls/{pr_info.id}/metadata'
87
- response = self._request('GET', api_url)
200
+ api_url = f"{self.base_url}/{pr_info.repository}/pulls/{pr_info.id}/metadata"
201
+ response = self._request("GET", api_url)
88
202
  if response.status_code == 204:
89
203
  return None
90
204
  if response.status_code != 200:
91
205
  raise RecceCloudException(
92
- message='Failed to get artifact metadata from Recce Cloud.',
206
+ message="Failed to get artifact metadata from Recce Cloud.",
93
207
  reason=response.text,
94
- status_code=response.status_code
208
+ status_code=response.status_code,
95
209
  )
96
210
  return response.json()
97
211
 
98
- def purge_artifacts(self, pr_info: PullRequestInfo):
99
- api_url = f'{self.base_url}/{pr_info.repository}/pulls/{pr_info.id}/artifacts'
100
- response = self._request('DELETE', api_url)
212
+ def purge_artifacts(self, repository: str, pr_id: int = None, branch: str = None):
213
+ if pr_id is not None:
214
+ api_url = f"{self.base_url}/{repository}/pulls/{pr_id}/artifacts"
215
+ error_message = "Failed to purge artifacts from Recce Cloud."
216
+ elif branch is not None:
217
+ api_url = f"{self.base_url}/{repository}/commits/{branch}/artifacts"
218
+ error_message = "Failed to delete artifacts from Recce Cloud."
219
+ else:
220
+ raise ValueError(
221
+ "Please either run this command from within a pull request context "
222
+ "or specify a branch using the --branch option."
223
+ )
224
+ response = self._request("DELETE", api_url)
101
225
  if response.status_code != 204:
102
226
  raise RecceCloudException(
103
- message='Failed to purge artifacts from Recce Cloud.',
227
+ message=error_message,
104
228
  reason=response.text,
105
- status_code=response.status_code
229
+ status_code=response.status_code,
106
230
  )
107
231
 
108
232
  def check_artifacts_exists(self, pr_info: PullRequestInfo) -> bool:
109
- api_url = f'{self.base_url}/{pr_info.repository}/pulls/{pr_info.id}/metadata'
110
- response = self._request('GET', api_url)
233
+ api_url = f"{self.base_url}/{pr_info.repository}/pulls/{pr_info.id}/metadata"
234
+ response = self._request("GET", api_url)
111
235
  if response.status_code == 200:
112
236
  return True
113
237
  elif response.status_code == 204:
114
238
  return False
115
239
  else:
116
240
  raise RecceCloudException(
117
- message='Failed to check if artifacts exist in Recce Cloud.',
241
+ message="Failed to check if artifacts exist in Recce Cloud.",
118
242
  reason=response.text,
119
- status_code=response.status_code
243
+ status_code=response.status_code,
120
244
  )
121
245
 
122
246
  def share_state(self, file_name: str, file_io: IO):
123
- api_url = f'{self.base_url}/recce-state/upload'
124
- files = {'file': (file_name, file_io, 'application/json')}
125
- response = self._request('POST', api_url, files=files)
247
+ api_url = f"{self.base_url}/recce-state/upload"
248
+ files = {"file": (file_name, file_io, "application/json")}
249
+ response = self._request("POST", api_url, files=files)
126
250
  if response.status_code == 403:
127
- return {'status': 'error', 'message': response.json().get('detail')}
251
+ return {"status": "error", "message": response.json().get("detail")}
128
252
  if response.status_code != 200:
129
253
  raise RecceCloudException(
130
- message='Failed to share Recce state.',
131
- reason=response.text,
132
- status_code=response.status_code
254
+ message="Failed to share Recce state.", reason=response.text, status_code=response.status_code
133
255
  )
134
256
  return response.json()
135
257
 
136
258
  def update_github_pull_request_check(self, pr_info: PullRequestInfo, metadata: dict = None):
137
- api_url = f'{self.base_url}/{pr_info.repository}/pulls/{pr_info.id}/github/checks'
259
+ api_url = f"{self.base_url}/{pr_info.repository}/pulls/{pr_info.id}/github/checks"
138
260
  try:
139
- self._request('POST', api_url, json=metadata)
261
+ self._request("POST", api_url, json=metadata)
140
262
  except Exception as e:
141
263
  # We don't care the response of this request, so we don't need to raise any exception.
142
- logger.debug(f'Failed to update the GitHub PR check. Reason: {str(e)}')
264
+ logger.debug(f"Failed to update the GitHub PR check. Reason: {str(e)}")
143
265
 
144
266
  def get_user_info(self) -> Dict:
145
- api_url = f'{self.base_url}/users'
146
- response = self._request('GET', api_url)
267
+ api_url = f"{self.base_url}/users"
268
+ response = self._request("GET", api_url)
147
269
  if response.status_code != 200:
148
270
  raise RecceCloudException(
149
- message='Failed to get user info from Recce Cloud.',
271
+ message="Failed to get user info from Recce Cloud.",
150
272
  reason=response.text,
151
- status_code=response.status_code
273
+ status_code=response.status_code,
152
274
  )
153
- return response.json().get('user')
275
+ return response.json().get("user")
154
276
 
155
277
  def set_onboarding_state(self, state: str):
156
- api_url = f'{self.base_url}/users/onboarding-state'
157
- response = self._request('PUT', api_url, json={'state': state})
278
+ api_url = f"{self.base_url}/users/onboarding-state"
279
+ try:
280
+ response = self._request("PUT", api_url, json={"state": state})
281
+ response.raise_for_status()
282
+ except requests.exceptions.HTTPError as e:
283
+ # Don't Raise an exception if setting onboarding_state fails
284
+ logger.warning(f"Failed to set Onboarding State in Recce Cloud. Reason: {str(e)}")
285
+ return
286
+
287
+ def get_session(self, session_id: str):
288
+ api_url = f"{self.base_url_v2}/sessions/{session_id}"
289
+ response = self._request("GET", api_url)
290
+ if response.status_code == 403:
291
+ return {"status": "error", "message": response.json().get("detail")}
292
+ if response.status_code != 200:
293
+ raise RecceCloudException(
294
+ message="Failed to get session from Recce Cloud.",
295
+ reason=response.text,
296
+ status_code=response.status_code,
297
+ )
298
+ data = response.json()
299
+ if data["success"] is not True:
300
+ raise RecceCloudException(
301
+ message="Failed to get session from Recce Cloud.",
302
+ reason=data.get("message", "Unknown error"),
303
+ status_code=response.status_code,
304
+ )
305
+ return data["session"]
306
+
307
+ def update_session(self, org_id: str, project_id: str, session_id: str, adapter_type: str):
308
+ api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}"
309
+ data = {"adapter_type": adapter_type}
310
+ response = self._request("PATCH", api_url, json=data)
311
+ if response.status_code == 403:
312
+ return {"status": "error", "message": response.json().get("detail")}
313
+ if response.status_code != 200:
314
+ raise RecceCloudException(
315
+ message="Failed to update session in Recce Cloud.",
316
+ reason=response.text,
317
+ status_code=response.status_code,
318
+ )
319
+ return response.json()
320
+
321
+ def get_download_urls_by_session_id(self, org_id: str, project_id: str, session_id: str) -> dict[str, str]:
322
+ api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/download-url"
323
+ response = self._request("GET", api_url)
324
+ if response.status_code != 200:
325
+ raise RecceCloudException(
326
+ message="Failed to download session from Recce Cloud.",
327
+ reason=response.text,
328
+ status_code=response.status_code,
329
+ )
330
+ data = response.json()
331
+ if data["presigned_urls"] is None:
332
+ raise RecceCloudException(
333
+ message="No presigned URLs returned from the server.",
334
+ reason="",
335
+ status_code=404,
336
+ )
337
+
338
+ presigned_urls = data["presigned_urls"]
339
+ for key, url in presigned_urls.items():
340
+ presigned_urls[key] = self._replace_localhost_with_docker_internal(url)
341
+ return presigned_urls
342
+
343
+ def get_base_session_download_urls(self, org_id: str, project_id: str) -> dict[str, str]:
344
+ """Get download URLs for the base session of a project."""
345
+ api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/base-session/download-url"
346
+ response = self._request("GET", api_url)
158
347
  if response.status_code != 200:
159
348
  raise RecceCloudException(
160
- message='Failed to update onboarding state in Recce Cloud.',
349
+ message="Failed to download base session from Recce Cloud.",
161
350
  reason=response.text,
162
- status_code=response.status_code
351
+ status_code=response.status_code,
352
+ )
353
+ data = response.json()
354
+ if data["presigned_urls"] is None:
355
+ raise RecceCloudException(
356
+ message="No presigned URLs returned from the server.",
357
+ reason="",
358
+ status_code=404,
163
359
  )
164
360
 
361
+ presigned_urls = data["presigned_urls"]
362
+ for key, url in presigned_urls.items():
363
+ presigned_urls[key] = self._replace_localhost_with_docker_internal(url)
364
+ return presigned_urls
365
+
366
+ def get_upload_urls_by_session_id(self, org_id: str, project_id: str, session_id: str) -> dict[str, str]:
367
+ api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/upload-url"
368
+ response = self._request("GET", api_url)
369
+ if response.status_code != 200:
370
+ raise RecceCloudException(
371
+ message="Failed to get upload URLs for session from Recce Cloud.",
372
+ reason=response.text,
373
+ status_code=response.status_code,
374
+ )
375
+ data = response.json()
376
+ if data["presigned_urls"] is None:
377
+ raise RecceCloudException(
378
+ message="No presigned URLs returned from the server.",
379
+ reason="",
380
+ status_code=404,
381
+ )
382
+
383
+ presigned_urls = data["presigned_urls"]
384
+ for key, url in presigned_urls.items():
385
+ presigned_urls[key] = self._replace_localhost_with_docker_internal(url)
386
+ return presigned_urls
387
+
388
+ def post_recce_state_uploaded_by_session_id(self, org_id: str, project_id: str, session_id: str):
389
+ api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/recce-state-uploaded"
390
+ response = self._request("POST", api_url)
391
+ if response.status_code != 204:
392
+ raise RecceCloudException(
393
+ message="Failed to notify state uploaded for session in Recce Cloud.",
394
+ reason=response.text,
395
+ status_code=response.status_code,
396
+ )
397
+
398
+ def list_organizations(self) -> list:
399
+ """List all organizations the user has access to."""
400
+ api_url = f"{self.base_url_v2}/organizations"
401
+ response = self._request("GET", api_url)
402
+ if response.status_code != 200:
403
+ raise RecceCloudException(
404
+ message="Failed to list organizations from Recce Cloud.",
405
+ reason=response.text,
406
+ status_code=response.status_code,
407
+ )
408
+ data = response.json()
409
+ return data.get("organizations", [])
410
+
411
+ def list_projects(self, org_id: str) -> list:
412
+ """List all projects in an organization."""
413
+ api_url = f"{self.base_url_v2}/organizations/{org_id}/projects"
414
+ response = self._request("GET", api_url)
415
+ if response.status_code != 200:
416
+ raise RecceCloudException(
417
+ message="Failed to list projects from Recce Cloud.",
418
+ reason=response.text,
419
+ status_code=response.status_code,
420
+ )
421
+ data = response.json()
422
+ return data.get("projects", [])
423
+
424
+ def list_sessions(self, org_id: str, project_id: str) -> list:
425
+ """List all sessions in a project."""
426
+ api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions"
427
+ response = self._request("GET", api_url)
428
+ if response.status_code != 200:
429
+ raise RecceCloudException(
430
+ message="Failed to list sessions from Recce Cloud.",
431
+ reason=response.text,
432
+ status_code=response.status_code,
433
+ )
434
+ data = response.json()
435
+ return data.get("sessions", [])
436
+
165
437
 
166
438
  def get_recce_cloud_onboarding_state(token: str) -> str:
439
+ if token and token.startswith("rct-"):
440
+ return "undefined"
441
+
167
442
  try:
168
443
  recce_cloud = RecceCloud(token)
169
444
  user_info = recce_cloud.get_user_info()
170
445
  if user_info:
171
- return user_info.get('onboarding_state')
446
+ return user_info.get("onboarding_state")
172
447
  except Exception as e:
173
448
  logger.debug(str(e))
174
- return 'undefined'
449
+ return "undefined"
175
450
 
176
451
 
177
452
  def set_recce_cloud_onboarding_state(token: str, new_state: str):
recce/util/singleton.py CHANGED
@@ -1,9 +1,9 @@
1
1
  class SingletonMeta(type):
2
2
  """
3
- The Singleton class can be implemented in different ways in Python. Some
4
- possible methods include: base class, decorator, metaclass. We will use the
5
- metaclass because it is best suited for this purpose.
6
- """
3
+ The Singleton class can be implemented in different ways in Python. Some
4
+ possible methods include: base class, decorator, metaclass. We will use the
5
+ metaclass because it is best suited for this purpose.
6
+ """
7
7
 
8
8
  _instances = {}
9
9