recce-nightly 1.9.0.20250623__py3-none-any.whl → 1.25.0.20251112a2066__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.
- recce/VERSION +1 -1
- recce/__init__.py +5 -0
- recce/adapter/dbt_adapter/__init__.py +318 -240
- recce/artifact.py +76 -3
- recce/cli.py +703 -71
- recce/config.py +3 -3
- recce/connect_to_cloud.py +138 -0
- recce/core.py +3 -3
- recce/data/404.html +1 -22
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +12 -0
- recce/data/_next/static/6LypcDXgyuSaiSCrsmUub/_buildManifest.js +11 -0
- recce/data/_next/static/6LypcDXgyuSaiSCrsmUub/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/0a2b2dd4b57049c2.js +1 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/24fd885c7180a612.js +1 -0
- recce/data/_next/static/chunks/27e66b2eab4adc32.js +19 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/917619ab62a32388.js +1 -0
- recce/data/_next/static/chunks/93ba5a62932b704f.js +4 -0
- recce/data/_next/static/chunks/a43a2a5e06d5a92b.js +1 -0
- recce/data/_next/static/chunks/a6c78b24bd8b84fc.js +1 -0
- recce/data/_next/static/chunks/b2610ba997ff8c4f.js +110 -0
- recce/data/_next/static/chunks/ba2d87265a68599d.css +2 -0
- recce/data/_next/static/chunks/c117fd1c1382dd83.js +11 -0
- recce/data/_next/static/chunks/c9425ca46eebdde9.js +1 -0
- recce/data/_next/static/chunks/cc8a9eadba012be0.css +6 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/e392ad92847c3e17.js +1 -0
- recce/data/_next/static/chunks/e4ce95efe88dae79.js +11 -0
- recce/data/_next/static/chunks/e69c777814fea6ed.js +2 -0
- recce/data/_next/static/chunks/turbopack-21cfd73037ff57ab.js +3 -0
- recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-cyrillic-800-normal.bd5c9f50.woff → montserrat-cyrillic-800-normal.f9d58125.woff} +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → montserrat-latin-800-normal.d5761935.woff} +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-latin-ext-800-normal.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
- recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._index.txt +8 -0
- recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
- recce/data/_not-found/__next._not-found.txt +4 -0
- recce/data/_not-found/__next._tree.txt +10 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +68 -0
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -8
- recce/event/__init__.py +9 -8
- recce/event/collector.py +6 -2
- recce/event/track.py +10 -0
- recce/github.py +1 -1
- recce/mcp_server.py +632 -0
- recce/models/types.py +23 -2
- recce/pull_request.py +1 -1
- recce/run.py +23 -16
- recce/server.py +194 -19
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +632 -0
- recce/state/const.py +26 -0
- recce/state/local.py +56 -0
- recce/state/state.py +119 -0
- recce/state/state_loader.py +174 -0
- recce/summary.py +2 -1
- recce/tasks/dataframe.py +59 -2
- recce/tasks/rowcount.py +4 -1
- recce/tasks/schema.py +4 -1
- recce/tasks/valuediff.py +1 -1
- recce/util/api_token.py +11 -2
- recce/util/breaking.py +9 -0
- recce/util/cll.py +1 -2
- recce/util/io.py +2 -2
- recce/util/lineage.py +19 -18
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +229 -5
- recce/yaml/__init__.py +2 -2
- recce_cloud/__init__.py +15 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +104 -0
- recce_cloud/api/client.py +150 -0
- recce_cloud/api/exceptions.py +26 -0
- recce_cloud/api/factory.py +63 -0
- recce_cloud/api/github.py +72 -0
- recce_cloud/api/gitlab.py +78 -0
- recce_cloud/artifact.py +57 -0
- recce_cloud/ci_providers/__init__.py +9 -0
- recce_cloud/ci_providers/base.py +82 -0
- recce_cloud/ci_providers/detector.py +147 -0
- recce_cloud/ci_providers/github_actions.py +136 -0
- recce_cloud/ci_providers/gitlab_ci.py +130 -0
- recce_cloud/cli.py +303 -0
- recce_cloud/upload.py +213 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/METADATA +31 -27
- recce_nightly-1.25.0.20251112a2066.dist-info/RECORD +178 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/test_dbt_cll.py +412 -79
- tests/recce_cloud/__init__.py +0 -0
- tests/recce_cloud/test_ci_providers.py +351 -0
- tests/recce_cloud/test_cli.py +372 -0
- tests/recce_cloud/test_client.py +273 -0
- tests/recce_cloud/test_platform_clients.py +279 -0
- tests/test_cli.py +106 -3
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_connect_to_cloud.py +82 -0
- tests/test_core.py +148 -3
- tests/test_mcp_server.py +332 -0
- tests/test_server.py +6 -6
- tests/test_summary.py +14 -6
- recce/data/_next/static/WrRUb3nV8BhAZG_R8kVma/_buildManifest.js +0 -1
- recce/data/_next/static/chunks/181-acc61ddada3bc0ca.js +0 -43
- recce/data/_next/static/chunks/1bff33f1-1ef85cf5e658a751.js +0 -1
- recce/data/_next/static/chunks/217-879a84d70f7a907c.js +0 -2
- recce/data/_next/static/chunks/29e3cc0d-60045b2e47aa3916.js +0 -1
- recce/data/_next/static/chunks/36e1c10d-8e7be4a6c1f6ab2d.js +0 -1
- recce/data/_next/static/chunks/3998a672-03adacad07b346ac.js +0 -1
- recce/data/_next/static/chunks/3a92ee20-1081c360214f9602.js +0 -1
- recce/data/_next/static/chunks/42-cd3c06533f5fd47c.js +0 -9
- recce/data/_next/static/chunks/450c323b-fd94e7ffaa4a5efa.js +0 -1
- recce/data/_next/static/chunks/47d8844f-929aed9b1c73a905.js +0 -1
- recce/data/_next/static/chunks/608-3b079b544e5d5f5e.js +0 -15
- recce/data/_next/static/chunks/6dc81886-adbfa45836061d79.js +0 -1
- recce/data/_next/static/chunks/7a8a3e83-edf6dc64b5d5f0a5.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-d5f0438edd5c2a5b.js +0 -1
- recce/data/_next/static/chunks/86730205-cfb14e3f051bab35.js +0 -1
- recce/data/_next/static/chunks/8d700b6a.8bb140898499c512.js +0 -1
- recce/data/_next/static/chunks/92-7ab55ae02606193c.js +0 -1
- recce/data/_next/static/chunks/9746af58-a42b7d169cacadf0.js +0 -1
- recce/data/_next/static/chunks/a30376cd-de84559016d7e133.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-01ed58b7f971d311.js +0 -1
- recce/data/_next/static/chunks/app/layout-177a410a97e0d018.js +0 -1
- recce/data/_next/static/chunks/app/page-59241c42b7dd4fcf.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-4282bdcf459e075c.js +0 -1
- recce/data/_next/static/chunks/bbda5537-9ec25eb1dd62348a.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-08cb668a789d6afd.js +0 -1
- recce/data/_next/static/chunks/ce84277d-2e5d1d46910cf052.js +0 -1
- recce/data/_next/static/chunks/febdd86e-c6b525341634b860.js +0 -54
- recce/data/_next/static/chunks/fee69bc6-2dbccaf9b90474e6.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-app-39061b0166c47f55.js +0 -1
- recce/data/_next/static/chunks/main-b5b3ae20a1405261.js +0 -1
- recce/data/_next/static/chunks/pages/_app-437c455677d62394.js +0 -1
- recce/data/_next/static/chunks/pages/_error-e7650df18ca04bde.js +0 -1
- recce/data/_next/static/chunks/webpack-7b49d5ba7e3a434d.js +0 -1
- recce/data/_next/static/css/17a96168e3a9db13.css +0 -1
- recce/data/_next/static/css/1b121dc4d36aeb4d.css +0 -3
- recce/data/_next/static/css/35c6679a098e1e34.css +0 -1
- recce/data/_next/static/css/951e2e0eea2d4a5b.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/state.py +0 -785
- recce_nightly-1.9.0.20250623.dist-info/RECORD +0 -151
- tests/test_state.py +0 -134
- /recce/data/_next/static/{WrRUb3nV8BhAZG_R8kVma → 6LypcDXgyuSaiSCrsmUub}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
- /recce/data/_next/static/media/{reload-image.79aabb7d.svg → reload-image.7aa931c7.svg} +0 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/WHEEL +0 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.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
|
@@ -12,6 +12,9 @@ from recce.pull_request import PullRequestInfo
|
|
|
12
12
|
RECCE_CLOUD_API_HOST = os.environ.get("RECCE_CLOUD_API_HOST", "https://cloud.datarecce.io")
|
|
13
13
|
RECCE_CLOUD_BASE_URL = os.environ.get("RECCE_CLOUD_BASE_URL", RECCE_CLOUD_API_HOST)
|
|
14
14
|
|
|
15
|
+
DOCKER_INTERNAL_URL_PREFIX = "http://host.docker.internal"
|
|
16
|
+
LOCALHOST_URL_PREFIX = "http://localhost"
|
|
17
|
+
|
|
15
18
|
logger = logging.getLogger("uvicorn")
|
|
16
19
|
|
|
17
20
|
|
|
@@ -39,6 +42,7 @@ class RecceCloud:
|
|
|
39
42
|
self.token = token
|
|
40
43
|
self.token_type = "github_token" if token.startswith(("ghp_", "gho_", "ghu_", "ghs_", "ghr_")) else "api_token"
|
|
41
44
|
self.base_url = f"{RECCE_CLOUD_API_HOST}/api/v1"
|
|
45
|
+
self.base_url_v2 = f"{RECCE_CLOUD_API_HOST}/api/v2"
|
|
42
46
|
|
|
43
47
|
def _request(self, method, url, headers: Dict = None, **kwargs):
|
|
44
48
|
headers = {
|
|
@@ -66,7 +70,7 @@ class RecceCloud:
|
|
|
66
70
|
pass
|
|
67
71
|
return False
|
|
68
72
|
|
|
69
|
-
def
|
|
73
|
+
def get_presigned_url_by_github_repo(
|
|
70
74
|
self,
|
|
71
75
|
method: PresignedUrlMethod,
|
|
72
76
|
repository: str,
|
|
@@ -78,7 +82,37 @@ class RecceCloud:
|
|
|
78
82
|
response = self._fetch_presigned_url(method, repository, artifact_name, metadata, pr_id, branch)
|
|
79
83
|
return response.get("presigned_url")
|
|
80
84
|
|
|
81
|
-
def
|
|
85
|
+
def _replace_localhost_with_docker_internal(self, url: str) -> str:
|
|
86
|
+
if url is None:
|
|
87
|
+
return None
|
|
88
|
+
if (
|
|
89
|
+
os.environ.get("RECCE_SHARE_INSTANCE_ENV") == "docker"
|
|
90
|
+
or os.environ.get("RECCE_TASK_INSTANCE_ENV") == "docker"
|
|
91
|
+
or os.environ.get("RECCE_INSTANCE_ENV") == "docker"
|
|
92
|
+
):
|
|
93
|
+
# For local development, convert the presigned URL from localhost to host.docker.internal
|
|
94
|
+
if url.startswith(LOCALHOST_URL_PREFIX):
|
|
95
|
+
return url.replace(LOCALHOST_URL_PREFIX, DOCKER_INTERNAL_URL_PREFIX)
|
|
96
|
+
return url
|
|
97
|
+
|
|
98
|
+
def get_presigned_url_by_share_id(
|
|
99
|
+
self,
|
|
100
|
+
method: PresignedUrlMethod,
|
|
101
|
+
share_id: str,
|
|
102
|
+
metadata: dict = None,
|
|
103
|
+
) -> str:
|
|
104
|
+
response = self._fetch_presigned_url_by_share_id(method, share_id, metadata=metadata)
|
|
105
|
+
presigned_url = response.get("presigned_url")
|
|
106
|
+
if not presigned_url:
|
|
107
|
+
raise RecceCloudException(
|
|
108
|
+
message="Failed to get presigned URL from Recce Cloud.",
|
|
109
|
+
reason="No presigned URL returned from the server.",
|
|
110
|
+
status_code=404,
|
|
111
|
+
)
|
|
112
|
+
presigned_url = self._replace_localhost_with_docker_internal(presigned_url)
|
|
113
|
+
return presigned_url
|
|
114
|
+
|
|
115
|
+
def get_download_presigned_url_by_github_repo_with_tags(
|
|
82
116
|
self, repository: str, artifact_name: str, branch: str = None
|
|
83
117
|
) -> (str, dict):
|
|
84
118
|
response = self._fetch_presigned_url(PresignedUrlMethod.DOWNLOAD, repository, artifact_name, branch=branch)
|
|
@@ -110,6 +144,33 @@ class RecceCloud:
|
|
|
110
144
|
)
|
|
111
145
|
return response.json()
|
|
112
146
|
|
|
147
|
+
def _fetch_presigned_url_by_share_id(
|
|
148
|
+
self,
|
|
149
|
+
method: PresignedUrlMethod,
|
|
150
|
+
share_id: str,
|
|
151
|
+
metadata: dict = None,
|
|
152
|
+
):
|
|
153
|
+
api_url = f"{self.base_url}/shares/{share_id}/presigned/{method}"
|
|
154
|
+
data = None
|
|
155
|
+
# Only provide metadata for upload requests
|
|
156
|
+
if method == PresignedUrlMethod.UPLOAD:
|
|
157
|
+
# Covert metadata values to strings to ensure JSON serializability
|
|
158
|
+
data = {"metadata": {key: str(value) for key, value in metadata.items()}} if metadata else None
|
|
159
|
+
response = self._request(
|
|
160
|
+
"POST",
|
|
161
|
+
api_url,
|
|
162
|
+
json=data,
|
|
163
|
+
)
|
|
164
|
+
if response.status_code != 200:
|
|
165
|
+
raise RecceCloudException(
|
|
166
|
+
message="Failed to {method} artifact {preposition} Recce Cloud.".format(
|
|
167
|
+
method=method, preposition="from" if method == PresignedUrlMethod.DOWNLOAD else "to"
|
|
168
|
+
),
|
|
169
|
+
reason=response.text,
|
|
170
|
+
status_code=response.status_code,
|
|
171
|
+
)
|
|
172
|
+
return response.json()
|
|
173
|
+
|
|
113
174
|
def get_artifact_metadata(self, pr_info: PullRequestInfo) -> dict:
|
|
114
175
|
api_url = f"{self.base_url}/{pr_info.repository}/pulls/{pr_info.id}/metadata"
|
|
115
176
|
response = self._request("GET", api_url)
|
|
@@ -123,12 +184,22 @@ class RecceCloud:
|
|
|
123
184
|
)
|
|
124
185
|
return response.json()
|
|
125
186
|
|
|
126
|
-
def purge_artifacts(self,
|
|
127
|
-
|
|
187
|
+
def purge_artifacts(self, repository: str, pr_id: int = None, branch: str = None):
|
|
188
|
+
if pr_id is not None:
|
|
189
|
+
api_url = f"{self.base_url}/{repository}/pulls/{pr_id}/artifacts"
|
|
190
|
+
error_message = "Failed to purge artifacts from Recce Cloud."
|
|
191
|
+
elif branch is not None:
|
|
192
|
+
api_url = f"{self.base_url}/{repository}/commits/{branch}/artifacts"
|
|
193
|
+
error_message = "Failed to delete artifacts from Recce Cloud."
|
|
194
|
+
else:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
"Please either run this command from within a pull request context "
|
|
197
|
+
"or specify a branch using the --branch option."
|
|
198
|
+
)
|
|
128
199
|
response = self._request("DELETE", api_url)
|
|
129
200
|
if response.status_code != 204:
|
|
130
201
|
raise RecceCloudException(
|
|
131
|
-
message=
|
|
202
|
+
message=error_message,
|
|
132
203
|
reason=response.text,
|
|
133
204
|
status_code=response.status_code,
|
|
134
205
|
)
|
|
@@ -188,8 +259,161 @@ class RecceCloud:
|
|
|
188
259
|
logger.warning(f"Failed to set Onboarding State in Recce Cloud. Reason: {str(e)}")
|
|
189
260
|
return
|
|
190
261
|
|
|
262
|
+
def get_session(self, session_id: str):
|
|
263
|
+
api_url = f"{self.base_url_v2}/sessions/{session_id}"
|
|
264
|
+
response = self._request("GET", api_url)
|
|
265
|
+
if response.status_code == 403:
|
|
266
|
+
return {"status": "error", "message": response.json().get("detail")}
|
|
267
|
+
if response.status_code != 200:
|
|
268
|
+
raise RecceCloudException(
|
|
269
|
+
message="Failed to get session from Recce Cloud.",
|
|
270
|
+
reason=response.text,
|
|
271
|
+
status_code=response.status_code,
|
|
272
|
+
)
|
|
273
|
+
data = response.json()
|
|
274
|
+
if data["success"] is not True:
|
|
275
|
+
raise RecceCloudException(
|
|
276
|
+
message="Failed to get session from Recce Cloud.",
|
|
277
|
+
reason=data.get("message", "Unknown error"),
|
|
278
|
+
status_code=response.status_code,
|
|
279
|
+
)
|
|
280
|
+
return data["session"]
|
|
281
|
+
|
|
282
|
+
def update_session(self, org_id: str, project_id: str, session_id: str, adapter_type: str):
|
|
283
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}"
|
|
284
|
+
data = {"adapter_type": adapter_type}
|
|
285
|
+
response = self._request("PATCH", api_url, json=data)
|
|
286
|
+
if response.status_code == 403:
|
|
287
|
+
return {"status": "error", "message": response.json().get("detail")}
|
|
288
|
+
if response.status_code != 200:
|
|
289
|
+
raise RecceCloudException(
|
|
290
|
+
message="Failed to update session in Recce Cloud.",
|
|
291
|
+
reason=response.text,
|
|
292
|
+
status_code=response.status_code,
|
|
293
|
+
)
|
|
294
|
+
return response.json()
|
|
295
|
+
|
|
296
|
+
def get_download_urls_by_session_id(self, org_id: str, project_id: str, session_id: str) -> dict[str, str]:
|
|
297
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/download-url"
|
|
298
|
+
response = self._request("GET", api_url)
|
|
299
|
+
if response.status_code != 200:
|
|
300
|
+
raise RecceCloudException(
|
|
301
|
+
message="Failed to download session from Recce Cloud.",
|
|
302
|
+
reason=response.text,
|
|
303
|
+
status_code=response.status_code,
|
|
304
|
+
)
|
|
305
|
+
data = response.json()
|
|
306
|
+
if data["presigned_urls"] is None:
|
|
307
|
+
raise RecceCloudException(
|
|
308
|
+
message="No presigned URLs returned from the server.",
|
|
309
|
+
reason="",
|
|
310
|
+
status_code=404,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
presigned_urls = data["presigned_urls"]
|
|
314
|
+
for key, url in presigned_urls.items():
|
|
315
|
+
presigned_urls[key] = self._replace_localhost_with_docker_internal(url)
|
|
316
|
+
return presigned_urls
|
|
317
|
+
|
|
318
|
+
def get_base_session_download_urls(self, org_id: str, project_id: str) -> dict[str, str]:
|
|
319
|
+
"""Get download URLs for the base session of a project."""
|
|
320
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/base-session/download-url"
|
|
321
|
+
response = self._request("GET", api_url)
|
|
322
|
+
if response.status_code != 200:
|
|
323
|
+
raise RecceCloudException(
|
|
324
|
+
message="Failed to download base session from Recce Cloud.",
|
|
325
|
+
reason=response.text,
|
|
326
|
+
status_code=response.status_code,
|
|
327
|
+
)
|
|
328
|
+
data = response.json()
|
|
329
|
+
if data["presigned_urls"] is None:
|
|
330
|
+
raise RecceCloudException(
|
|
331
|
+
message="No presigned URLs returned from the server.",
|
|
332
|
+
reason="",
|
|
333
|
+
status_code=404,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
presigned_urls = data["presigned_urls"]
|
|
337
|
+
for key, url in presigned_urls.items():
|
|
338
|
+
presigned_urls[key] = self._replace_localhost_with_docker_internal(url)
|
|
339
|
+
return presigned_urls
|
|
340
|
+
|
|
341
|
+
def get_upload_urls_by_session_id(self, org_id: str, project_id: str, session_id: str) -> dict[str, str]:
|
|
342
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/upload-url"
|
|
343
|
+
response = self._request("GET", api_url)
|
|
344
|
+
if response.status_code != 200:
|
|
345
|
+
raise RecceCloudException(
|
|
346
|
+
message="Failed to get upload URLs for session from Recce Cloud.",
|
|
347
|
+
reason=response.text,
|
|
348
|
+
status_code=response.status_code,
|
|
349
|
+
)
|
|
350
|
+
data = response.json()
|
|
351
|
+
if data["presigned_urls"] is None:
|
|
352
|
+
raise RecceCloudException(
|
|
353
|
+
message="No presigned URLs returned from the server.",
|
|
354
|
+
reason="",
|
|
355
|
+
status_code=404,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
presigned_urls = data["presigned_urls"]
|
|
359
|
+
for key, url in presigned_urls.items():
|
|
360
|
+
presigned_urls[key] = self._replace_localhost_with_docker_internal(url)
|
|
361
|
+
return presigned_urls
|
|
362
|
+
|
|
363
|
+
def post_recce_state_uploaded_by_session_id(self, org_id: str, project_id: str, session_id: str):
|
|
364
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/recce-state-uploaded"
|
|
365
|
+
response = self._request("POST", api_url)
|
|
366
|
+
if response.status_code != 204:
|
|
367
|
+
raise RecceCloudException(
|
|
368
|
+
message="Failed to notify state uploaded for session in Recce Cloud.",
|
|
369
|
+
reason=response.text,
|
|
370
|
+
status_code=response.status_code,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def list_organizations(self) -> list:
|
|
374
|
+
"""List all organizations the user has access to."""
|
|
375
|
+
api_url = f"{self.base_url_v2}/organizations"
|
|
376
|
+
response = self._request("GET", api_url)
|
|
377
|
+
if response.status_code != 200:
|
|
378
|
+
raise RecceCloudException(
|
|
379
|
+
message="Failed to list organizations from Recce Cloud.",
|
|
380
|
+
reason=response.text,
|
|
381
|
+
status_code=response.status_code,
|
|
382
|
+
)
|
|
383
|
+
data = response.json()
|
|
384
|
+
return data.get("organizations", [])
|
|
385
|
+
|
|
386
|
+
def list_projects(self, org_id: str) -> list:
|
|
387
|
+
"""List all projects in an organization."""
|
|
388
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects"
|
|
389
|
+
response = self._request("GET", api_url)
|
|
390
|
+
if response.status_code != 200:
|
|
391
|
+
raise RecceCloudException(
|
|
392
|
+
message="Failed to list projects from Recce Cloud.",
|
|
393
|
+
reason=response.text,
|
|
394
|
+
status_code=response.status_code,
|
|
395
|
+
)
|
|
396
|
+
data = response.json()
|
|
397
|
+
return data.get("projects", [])
|
|
398
|
+
|
|
399
|
+
def list_sessions(self, org_id: str, project_id: str) -> list:
|
|
400
|
+
"""List all sessions in a project."""
|
|
401
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions"
|
|
402
|
+
response = self._request("GET", api_url)
|
|
403
|
+
if response.status_code != 200:
|
|
404
|
+
raise RecceCloudException(
|
|
405
|
+
message="Failed to list sessions from Recce Cloud.",
|
|
406
|
+
reason=response.text,
|
|
407
|
+
status_code=response.status_code,
|
|
408
|
+
)
|
|
409
|
+
data = response.json()
|
|
410
|
+
return data.get("sessions", [])
|
|
411
|
+
|
|
191
412
|
|
|
192
413
|
def get_recce_cloud_onboarding_state(token: str) -> str:
|
|
414
|
+
if token and token.startswith("rct-"):
|
|
415
|
+
return "undefined"
|
|
416
|
+
|
|
193
417
|
try:
|
|
194
418
|
recce_cloud = RecceCloud(token)
|
|
195
419
|
user_info = recce_cloud.get_user_info()
|
recce/yaml/__init__.py
CHANGED
|
@@ -34,7 +34,7 @@ def dump(data, stream: Any = None, *, transform: Any = None) -> Any:
|
|
|
34
34
|
|
|
35
35
|
def safe_load_yaml(file_path):
|
|
36
36
|
try:
|
|
37
|
-
with open(file_path, "r") as f:
|
|
37
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
38
38
|
payload = safe_load(f)
|
|
39
39
|
except yaml.YAMLError as e:
|
|
40
40
|
print(e)
|
|
@@ -45,7 +45,7 @@ def safe_load_yaml(file_path):
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def round_trip_load_yaml(file_path):
|
|
48
|
-
with open(file_path, "r") as f:
|
|
48
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
49
49
|
try:
|
|
50
50
|
payload = load(f)
|
|
51
51
|
except yaml.YAMLError as e:
|
recce_cloud/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Recce Cloud - Lightweight CLI for Recce Cloud operations."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_version():
|
|
7
|
+
"""Get version from main recce VERSION file."""
|
|
8
|
+
# Reference the VERSION file from main recce package
|
|
9
|
+
version_file = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "recce", "VERSION"))
|
|
10
|
+
with open(version_file) as fh:
|
|
11
|
+
version = fh.read().strip()
|
|
12
|
+
return version
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__version__ = get_version()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Recce Cloud API client module."""
|
|
2
|
+
|
|
3
|
+
from recce_cloud.api.base import BaseRecceCloudClient
|
|
4
|
+
from recce_cloud.api.client import RecceCloudClient
|
|
5
|
+
from recce_cloud.api.exceptions import RecceCloudException
|
|
6
|
+
from recce_cloud.api.factory import create_platform_client
|
|
7
|
+
from recce_cloud.api.github import GitHubRecceCloudClient
|
|
8
|
+
from recce_cloud.api.gitlab import GitLabRecceCloudClient
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BaseRecceCloudClient",
|
|
12
|
+
"RecceCloudClient",
|
|
13
|
+
"RecceCloudException",
|
|
14
|
+
"create_platform_client",
|
|
15
|
+
"GitHubRecceCloudClient",
|
|
16
|
+
"GitLabRecceCloudClient",
|
|
17
|
+
]
|
recce_cloud/api/base.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base API client for Recce Cloud.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Dict, Optional
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from recce_cloud.api.exceptions import RecceCloudException
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseRecceCloudClient(ABC):
|
|
15
|
+
"""Abstract base class for platform-specific Recce Cloud API clients."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, token: str, api_host: Optional[str] = None):
|
|
18
|
+
"""
|
|
19
|
+
Initialize the API client.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
token: Authentication token (GITHUB_TOKEN, CI_JOB_TOKEN, or RECCE_API_TOKEN)
|
|
23
|
+
api_host: Recce Cloud API host (defaults to RECCE_CLOUD_API_HOST or https://cloud.datarecce.io)
|
|
24
|
+
"""
|
|
25
|
+
self.token = token
|
|
26
|
+
self.api_host = api_host or os.getenv("RECCE_CLOUD_API_HOST", "https://cloud.datarecce.io")
|
|
27
|
+
|
|
28
|
+
def _make_request(self, method: str, url: str, **kwargs) -> Dict:
|
|
29
|
+
"""
|
|
30
|
+
Make an HTTP request to Recce Cloud API.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
method: HTTP method (GET, POST, PUT, etc.)
|
|
34
|
+
url: Full URL for the request
|
|
35
|
+
**kwargs: Additional arguments passed to requests
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Response JSON as dictionary
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
RecceCloudException: If the request fails
|
|
42
|
+
"""
|
|
43
|
+
headers = kwargs.pop("headers", {})
|
|
44
|
+
headers.update(
|
|
45
|
+
{
|
|
46
|
+
"Authorization": f"Bearer {self.token}",
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
response = requests.request(method, url, headers=headers, **kwargs)
|
|
53
|
+
response.raise_for_status()
|
|
54
|
+
return response.json()
|
|
55
|
+
except requests.exceptions.HTTPError as e:
|
|
56
|
+
reason = str(e)
|
|
57
|
+
if e.response is not None:
|
|
58
|
+
try:
|
|
59
|
+
error_detail = e.response.json()
|
|
60
|
+
reason = error_detail.get("message", str(e))
|
|
61
|
+
except Exception:
|
|
62
|
+
reason = e.response.text or str(e)
|
|
63
|
+
raise RecceCloudException(reason=reason, status_code=e.response.status_code if e.response else None)
|
|
64
|
+
except requests.exceptions.RequestException as e:
|
|
65
|
+
raise RecceCloudException(reason=str(e))
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def touch_recce_session(
|
|
69
|
+
self,
|
|
70
|
+
branch: str,
|
|
71
|
+
adapter_type: str,
|
|
72
|
+
cr_number: Optional[int] = None,
|
|
73
|
+
commit_sha: Optional[str] = None,
|
|
74
|
+
) -> Dict:
|
|
75
|
+
"""
|
|
76
|
+
Create or touch a Recce session.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
branch: Branch name
|
|
80
|
+
adapter_type: DBT adapter type (e.g., 'postgres', 'snowflake', 'bigquery')
|
|
81
|
+
cr_number: Change request number (PR/MR number) for CR sessions
|
|
82
|
+
commit_sha: Commit SHA (GitLab requires this)
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dictionary containing:
|
|
86
|
+
- session_id: Session ID
|
|
87
|
+
- manifest_upload_url: Presigned URL for manifest.json upload
|
|
88
|
+
- catalog_upload_url: Presigned URL for catalog.json upload
|
|
89
|
+
"""
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def upload_completed(self, session_id: str, commit_sha: Optional[str] = None) -> Dict:
|
|
94
|
+
"""
|
|
95
|
+
Notify Recce Cloud that upload is complete.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
session_id: Session ID from touch_recce_session
|
|
99
|
+
commit_sha: Commit SHA (GitLab requires this)
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Empty dictionary or acknowledgement
|
|
103
|
+
"""
|
|
104
|
+
pass
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recce Cloud API client for lightweight operations.
|
|
3
|
+
|
|
4
|
+
Simplified version of recce.util.recce_cloud.RecceCloud with only
|
|
5
|
+
the methods needed for upload-session functionality.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from recce_cloud.api.exceptions import RecceCloudException
|
|
13
|
+
|
|
14
|
+
RECCE_CLOUD_API_HOST = os.environ.get("RECCE_CLOUD_API_HOST", "https://cloud.datarecce.io")
|
|
15
|
+
|
|
16
|
+
DOCKER_INTERNAL_URL_PREFIX = "http://host.docker.internal"
|
|
17
|
+
LOCALHOST_URL_PREFIX = "http://localhost"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RecceCloudClient:
|
|
21
|
+
"""
|
|
22
|
+
Lightweight Recce Cloud API client.
|
|
23
|
+
|
|
24
|
+
Supports authentication with Recce Cloud API token (starts with "rct-").
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, token: str):
|
|
28
|
+
if token is None:
|
|
29
|
+
raise ValueError("Token cannot be None.")
|
|
30
|
+
self.token = token
|
|
31
|
+
self.base_url_v2 = f"{RECCE_CLOUD_API_HOST}/api/v2"
|
|
32
|
+
|
|
33
|
+
def _request(self, method: str, url: str, headers: dict = None, **kwargs):
|
|
34
|
+
"""Make authenticated HTTP request to Recce Cloud API."""
|
|
35
|
+
headers = {
|
|
36
|
+
**(headers or {}),
|
|
37
|
+
"Authorization": f"Bearer {self.token}",
|
|
38
|
+
}
|
|
39
|
+
return requests.request(method, url, headers=headers, **kwargs)
|
|
40
|
+
|
|
41
|
+
def _replace_localhost_with_docker_internal(self, url: str) -> str:
|
|
42
|
+
"""Convert localhost URLs to docker internal URLs if running in Docker."""
|
|
43
|
+
if url is None:
|
|
44
|
+
return None
|
|
45
|
+
if (
|
|
46
|
+
os.environ.get("RECCE_SHARE_INSTANCE_ENV") == "docker"
|
|
47
|
+
or os.environ.get("RECCE_TASK_INSTANCE_ENV") == "docker"
|
|
48
|
+
or os.environ.get("RECCE_INSTANCE_ENV") == "docker"
|
|
49
|
+
):
|
|
50
|
+
# For local development, convert the presigned URL from localhost to host.docker.internal
|
|
51
|
+
if url.startswith(LOCALHOST_URL_PREFIX):
|
|
52
|
+
return url.replace(LOCALHOST_URL_PREFIX, DOCKER_INTERNAL_URL_PREFIX)
|
|
53
|
+
return url
|
|
54
|
+
|
|
55
|
+
def get_session(self, session_id: str) -> dict:
|
|
56
|
+
"""
|
|
57
|
+
Get session information from Recce Cloud.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
session_id: The session ID to retrieve
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
dict containing session information with keys:
|
|
64
|
+
- org_id: Organization ID
|
|
65
|
+
- project_id: Project ID
|
|
66
|
+
- ... other session fields
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
RecceCloudException: If the request fails
|
|
70
|
+
"""
|
|
71
|
+
api_url = f"{self.base_url_v2}/sessions/{session_id}"
|
|
72
|
+
response = self._request("GET", api_url)
|
|
73
|
+
if response.status_code == 403:
|
|
74
|
+
return {"status": "error", "message": response.json().get("detail")}
|
|
75
|
+
if response.status_code != 200:
|
|
76
|
+
raise RecceCloudException(
|
|
77
|
+
reason=response.text,
|
|
78
|
+
status_code=response.status_code,
|
|
79
|
+
)
|
|
80
|
+
data = response.json()
|
|
81
|
+
if data["success"] is not True:
|
|
82
|
+
raise RecceCloudException(
|
|
83
|
+
reason=data.get("message", "Unknown error"),
|
|
84
|
+
status_code=response.status_code,
|
|
85
|
+
)
|
|
86
|
+
return data["session"]
|
|
87
|
+
|
|
88
|
+
def get_upload_urls_by_session_id(self, org_id: str, project_id: str, session_id: str) -> dict:
|
|
89
|
+
"""
|
|
90
|
+
Get presigned S3 upload URLs for a session.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
org_id: Organization ID
|
|
94
|
+
project_id: Project ID
|
|
95
|
+
session_id: Session ID
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
dict with keys:
|
|
99
|
+
- manifest_url: Presigned URL for uploading manifest.json
|
|
100
|
+
- catalog_url: Presigned URL for uploading catalog.json
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
RecceCloudException: If the request fails
|
|
104
|
+
"""
|
|
105
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}/upload-url"
|
|
106
|
+
response = self._request("GET", api_url)
|
|
107
|
+
if response.status_code != 200:
|
|
108
|
+
raise RecceCloudException(
|
|
109
|
+
reason=response.text,
|
|
110
|
+
status_code=response.status_code,
|
|
111
|
+
)
|
|
112
|
+
data = response.json()
|
|
113
|
+
if data["presigned_urls"] is None:
|
|
114
|
+
raise RecceCloudException(
|
|
115
|
+
reason="No presigned URLs returned from the server.",
|
|
116
|
+
status_code=404,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
presigned_urls = data["presigned_urls"]
|
|
120
|
+
for key, url in presigned_urls.items():
|
|
121
|
+
presigned_urls[key] = self._replace_localhost_with_docker_internal(url)
|
|
122
|
+
return presigned_urls
|
|
123
|
+
|
|
124
|
+
def update_session(self, org_id: str, project_id: str, session_id: str, adapter_type: str) -> dict:
|
|
125
|
+
"""
|
|
126
|
+
Update session metadata with adapter type.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
org_id: Organization ID
|
|
130
|
+
project_id: Project ID
|
|
131
|
+
session_id: Session ID
|
|
132
|
+
adapter_type: dbt adapter type (e.g., "postgres", "snowflake", "bigquery")
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
dict containing updated session information
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
RecceCloudException: If the request fails
|
|
139
|
+
"""
|
|
140
|
+
api_url = f"{self.base_url_v2}/organizations/{org_id}/projects/{project_id}/sessions/{session_id}"
|
|
141
|
+
data = {"adapter_type": adapter_type}
|
|
142
|
+
response = self._request("PATCH", api_url, json=data)
|
|
143
|
+
if response.status_code == 403:
|
|
144
|
+
return {"status": "error", "message": response.json().get("detail")}
|
|
145
|
+
if response.status_code != 200:
|
|
146
|
+
raise RecceCloudException(
|
|
147
|
+
reason=response.text,
|
|
148
|
+
status_code=response.status_code,
|
|
149
|
+
)
|
|
150
|
+
return response.json()
|