recce-nightly 1.11.0.20250708__py3-none-any.whl → 1.12.0.20250710__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.
- recce/VERSION +1 -1
- recce/adapter/dbt_adapter/__init__.py +5 -1
- recce/artifact.py +2 -2
- recce/cli.py +26 -16
- recce/core.py +1 -1
- recce/data/404.html +1 -1
- recce/data/_next/static/chunks/583-0138cef66612d810.js +1 -0
- recce/data/_next/static/chunks/71-a829703f4f507654.js +10 -0
- recce/data/_next/static/chunks/711-61299a23b04d7eb9.js +30 -0
- recce/data/_next/static/chunks/app/layout-500042ac66908cd1.js +1 -0
- recce/data/_next/static/chunks/app/page-84051436d36d271c.js +1 -0
- recce/data/_next/static/chunks/{webpack-b5a9818d6d67641f.js → webpack-e8321d14a0e3d7fd.js} +1 -1
- recce/data/_next/static/css/d4ebbb3f53edc04c.css +14 -0
- recce/data/index.html +1 -1
- recce/data/index.txt +4 -4
- recce/models/types.py +3 -0
- recce/state.py +154 -75
- recce/util/api_token.py +3 -1
- recce/util/recce_cloud.py +38 -2
- {recce_nightly-1.11.0.20250708.dist-info → recce_nightly-1.12.0.20250710.dist-info}/METADATA +1 -1
- {recce_nightly-1.11.0.20250708.dist-info → recce_nightly-1.12.0.20250710.dist-info}/RECORD +28 -28
- tests/adapter/dbt_adapter/test_dbt_cll.py +31 -18
- recce/data/_next/static/chunks/488-74d3038680fbbbd1.js +0 -30
- recce/data/_next/static/chunks/512-5f44b9448f104530.js +0 -10
- recce/data/_next/static/chunks/583-bafc31cf92748715.js +0 -1
- recce/data/_next/static/chunks/app/layout-aa349a34c05b8677.js +0 -1
- recce/data/_next/static/chunks/app/page-e7d0a50c2a7f36f6.js +0 -1
- recce/data/_next/static/css/2d0bc492ccebdbed.css +0 -14
- /recce/data/_next/static/{L_GMfqmQNYzCO_95xuR_2 → 2qailYtnp-4OxNmY4TOHo}/_buildManifest.js +0 -0
- /recce/data/_next/static/{L_GMfqmQNYzCO_95xuR_2 → 2qailYtnp-4OxNmY4TOHo}/_ssgManifest.js +0 -0
- {recce_nightly-1.11.0.20250708.dist-info → recce_nightly-1.12.0.20250710.dist-info}/WHEEL +0 -0
- {recce_nightly-1.11.0.20250708.dist-info → recce_nightly-1.12.0.20250710.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.11.0.20250708.dist-info → recce_nightly-1.12.0.20250710.dist-info}/licenses/LICENSE +0 -0
- {recce_nightly-1.11.0.20250708.dist-info → recce_nightly-1.12.0.20250710.dist-info}/top_level.txt +0 -0
recce/state.py
CHANGED
|
@@ -9,7 +9,7 @@ from base64 import b64encode
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from datetime import datetime
|
|
11
11
|
from hashlib import md5, sha256
|
|
12
|
-
from typing import Dict, List, Optional, Tuple, Union
|
|
12
|
+
from typing import Dict, List, Literal, Optional, Tuple, Union
|
|
13
13
|
from urllib.parse import urlencode
|
|
14
14
|
|
|
15
15
|
import botocore.exceptions
|
|
@@ -17,6 +17,7 @@ from pydantic import BaseModel, Field
|
|
|
17
17
|
|
|
18
18
|
from recce import get_version
|
|
19
19
|
from recce.event import get_recce_api_token
|
|
20
|
+
from recce.exceptions import RecceException
|
|
20
21
|
from recce.git import current_branch
|
|
21
22
|
from recce.models import CheckDAO
|
|
22
23
|
from recce.models.types import Check, Run
|
|
@@ -136,7 +137,7 @@ class RecceState(BaseModel):
|
|
|
136
137
|
if metadata.schema_version == "v0":
|
|
137
138
|
pass
|
|
138
139
|
else:
|
|
139
|
-
raise
|
|
140
|
+
raise RecceException(f"Unsupported state file version: {metadata.schema_version}")
|
|
140
141
|
return state
|
|
141
142
|
|
|
142
143
|
@staticmethod
|
|
@@ -203,28 +204,50 @@ class RecceStateLoader:
|
|
|
203
204
|
self.state_lock = threading.Lock()
|
|
204
205
|
self.state_etag = None
|
|
205
206
|
self.pr_info = None
|
|
207
|
+
self.catalog: Literal["github", "preview"] = "github"
|
|
208
|
+
self.share_id = None
|
|
206
209
|
|
|
207
210
|
if self.cloud_mode:
|
|
208
|
-
if
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
if self.cloud_options.get("github_token"):
|
|
212
|
+
self.catalog = "github"
|
|
213
|
+
self.pr_info = fetch_pr_metadata(
|
|
214
|
+
cloud=self.cloud_mode, github_token=self.cloud_options.get("github_token")
|
|
215
|
+
)
|
|
216
|
+
if self.pr_info.id is None:
|
|
217
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
218
|
+
elif self.cloud_options.get("api_token"):
|
|
219
|
+
self.catalog = "preview"
|
|
220
|
+
self.share_id = self.cloud_options.get("share_id")
|
|
221
|
+
else:
|
|
222
|
+
raise RecceException(RECCE_CLOUD_TOKEN_MISSING.error_message)
|
|
213
223
|
|
|
214
224
|
# Load the state
|
|
215
225
|
self.load()
|
|
216
226
|
|
|
217
227
|
def verify(self) -> bool:
|
|
218
228
|
if self.cloud_mode:
|
|
219
|
-
if self.
|
|
220
|
-
self.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if not self.cloud_options.get("host"):
|
|
224
|
-
if self.cloud_options.get("password") is None:
|
|
225
|
-
self.error_message = RECCE_CLOUD_PASSWORD_MISSING.error_message
|
|
226
|
-
self.hint_message = RECCE_CLOUD_PASSWORD_MISSING.hint_message
|
|
229
|
+
if self.catalog == "github":
|
|
230
|
+
if self.cloud_options.get("github_token") is None:
|
|
231
|
+
self.error_message = RECCE_CLOUD_TOKEN_MISSING.error_message
|
|
232
|
+
self.hint_message = RECCE_CLOUD_TOKEN_MISSING.hint_message
|
|
227
233
|
return False
|
|
234
|
+
if not self.cloud_options.get("host"):
|
|
235
|
+
if self.cloud_options.get("password") is None:
|
|
236
|
+
self.error_message = RECCE_CLOUD_PASSWORD_MISSING.error_message
|
|
237
|
+
self.hint_message = RECCE_CLOUD_PASSWORD_MISSING.hint_message
|
|
238
|
+
return False
|
|
239
|
+
elif self.catalog == "preview":
|
|
240
|
+
if self.cloud_options.get("api_token") is None:
|
|
241
|
+
self.error_message = RECCE_API_TOKEN_MISSING.error_message
|
|
242
|
+
self.hint_message = RECCE_API_TOKEN_MISSING.hint_message
|
|
243
|
+
return False
|
|
244
|
+
if self.cloud_options.get("share_id") is None:
|
|
245
|
+
self.error_message = "No share ID is provided for the preview catalog."
|
|
246
|
+
self.hint_message = (
|
|
247
|
+
'Please provide a share URL in the command argument with option "--share-url <share-url>"'
|
|
248
|
+
)
|
|
249
|
+
return False
|
|
250
|
+
|
|
228
251
|
else:
|
|
229
252
|
if self.review_mode is True and self.state_file is None:
|
|
230
253
|
self.error_message = "Recce can not launch without a state file."
|
|
@@ -233,6 +256,10 @@ class RecceStateLoader:
|
|
|
233
256
|
pass
|
|
234
257
|
return True
|
|
235
258
|
|
|
259
|
+
@property
|
|
260
|
+
def token(self):
|
|
261
|
+
return self.cloud_options.get("github_token") or self.cloud_options.get("api_token")
|
|
262
|
+
|
|
236
263
|
@property
|
|
237
264
|
def error_and_hint(self) -> (Union[str, None], Union[str, None]):
|
|
238
265
|
return self.error_message, self.hint_message
|
|
@@ -348,8 +375,12 @@ class RecceStateLoader:
|
|
|
348
375
|
RecceState: The state object.
|
|
349
376
|
str: The etag of the state file.
|
|
350
377
|
"""
|
|
351
|
-
if
|
|
352
|
-
|
|
378
|
+
if self.catalog == "github":
|
|
379
|
+
if (self.pr_info is None) or (self.pr_info.id is None) or (self.pr_info.repository is None):
|
|
380
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
381
|
+
elif self.catalog == "preview":
|
|
382
|
+
if self.share_id is None:
|
|
383
|
+
raise RecceException("Cannot load the share state from Recce Cloud. No share ID is provided.")
|
|
353
384
|
|
|
354
385
|
if self.cloud_options.get("host", "").startswith("s3://"):
|
|
355
386
|
logger.debug("Fetching state from AWS S3 bucket...")
|
|
@@ -357,49 +388,60 @@ class RecceStateLoader:
|
|
|
357
388
|
else:
|
|
358
389
|
logger.debug("Fetching state from Recce Cloud...")
|
|
359
390
|
metadata = self._get_metadata_from_recce_cloud()
|
|
360
|
-
if metadata
|
|
361
|
-
|
|
362
|
-
|
|
391
|
+
if metadata:
|
|
392
|
+
state_etag = metadata.get("etag")
|
|
393
|
+
else:
|
|
394
|
+
state_etag = None
|
|
363
395
|
if self.state_etag and state_etag == self.state_etag:
|
|
364
396
|
return self.state, self.state_etag
|
|
365
397
|
|
|
366
398
|
return self._load_state_from_recce_cloud(), state_etag
|
|
367
399
|
|
|
368
400
|
def _get_metadata_from_recce_cloud(self) -> Union[dict, None]:
|
|
369
|
-
recce_cloud = RecceCloud(token=self.
|
|
370
|
-
return recce_cloud.get_artifact_metadata(pr_info=self.pr_info)
|
|
401
|
+
recce_cloud = RecceCloud(token=self.token)
|
|
402
|
+
return recce_cloud.get_artifact_metadata(pr_info=self.pr_info) if self.pr_info else None
|
|
371
403
|
|
|
372
404
|
def _load_state_from_recce_cloud(self) -> Union[RecceState, None]:
|
|
373
405
|
import tempfile
|
|
374
406
|
|
|
375
407
|
import requests
|
|
376
408
|
|
|
377
|
-
recce_cloud = RecceCloud(token=self.
|
|
378
|
-
|
|
379
|
-
method=PresignedUrlMethod.DOWNLOAD,
|
|
380
|
-
pr_id=self.pr_info.id,
|
|
381
|
-
repository=self.pr_info.repository,
|
|
382
|
-
artifact_name=RECCE_STATE_COMPRESSED_FILE,
|
|
383
|
-
)
|
|
409
|
+
recce_cloud = RecceCloud(token=self.token)
|
|
410
|
+
password = None
|
|
384
411
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
412
|
+
if self.catalog == "github":
|
|
413
|
+
presigned_url = recce_cloud.get_presigned_url_by_github_repo(
|
|
414
|
+
method=PresignedUrlMethod.DOWNLOAD,
|
|
415
|
+
pr_id=self.pr_info.id,
|
|
416
|
+
repository=self.pr_info.repository,
|
|
417
|
+
artifact_name=RECCE_STATE_COMPRESSED_FILE,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
password = self.cloud_options.get("password")
|
|
421
|
+
if password is None:
|
|
422
|
+
raise RecceException(RECCE_CLOUD_PASSWORD_MISSING.error_message)
|
|
423
|
+
elif self.catalog == "preview":
|
|
424
|
+
share_id = self.cloud_options.get("share_id")
|
|
425
|
+
presigned_url = recce_cloud.get_presigned_url_by_share_id(
|
|
426
|
+
method=PresignedUrlMethod.DOWNLOAD, share_id=share_id
|
|
427
|
+
)
|
|
388
428
|
|
|
389
429
|
with tempfile.NamedTemporaryFile() as tmp:
|
|
390
|
-
headers = s3_sse_c_headers(password)
|
|
430
|
+
headers = s3_sse_c_headers(password) if password else None
|
|
391
431
|
response = requests.get(presigned_url, headers=headers)
|
|
392
432
|
if response.status_code == 404:
|
|
393
433
|
self.error_message = "The state file is not found in Recce Cloud."
|
|
394
434
|
return None
|
|
395
435
|
elif response.status_code != 200:
|
|
396
436
|
self.error_message = response.text
|
|
397
|
-
raise
|
|
437
|
+
raise RecceException(
|
|
398
438
|
f"{response.status_code} Failed to download the state file from Recce Cloud. The password could be wrong."
|
|
399
439
|
)
|
|
400
440
|
with open(tmp.name, "wb") as f:
|
|
401
441
|
f.write(response.content)
|
|
402
|
-
|
|
442
|
+
|
|
443
|
+
file_type = SupportedFileTypes.GZIP if self.catalog == "github" else SupportedFileTypes.FILE
|
|
444
|
+
return RecceState.from_file(tmp.name, file_type=file_type)
|
|
403
445
|
|
|
404
446
|
def _load_state_from_s3_bucket(self) -> Union[RecceState, None]:
|
|
405
447
|
import tempfile
|
|
@@ -408,11 +450,19 @@ class RecceStateLoader:
|
|
|
408
450
|
|
|
409
451
|
s3_client = boto3.client("s3")
|
|
410
452
|
s3_bucket_name = self.cloud_options.get("host").replace("s3://", "")
|
|
411
|
-
|
|
453
|
+
|
|
454
|
+
if self.catalog == "github":
|
|
455
|
+
s3_bucket_key = (
|
|
456
|
+
f"{self.catalog}/{self.pr_info.repository}/pulls/{self.pr_info.id}/{RECCE_STATE_COMPRESSED_FILE}"
|
|
457
|
+
)
|
|
458
|
+
elif self.catalog == "preview":
|
|
459
|
+
s3_bucket_key = f"{self.catalog}/{self.share_id}/{RECCE_STATE_FILE}"
|
|
460
|
+
else:
|
|
461
|
+
raise RecceException(f"Unsupported catalog type. {self.catalog} is not supported.")
|
|
412
462
|
|
|
413
463
|
rc, error_message = check_s3_bucket(s3_bucket_name)
|
|
414
464
|
if rc is False:
|
|
415
|
-
raise
|
|
465
|
+
raise RecceException(error_message)
|
|
416
466
|
|
|
417
467
|
with tempfile.NamedTemporaryFile() as tmp:
|
|
418
468
|
try:
|
|
@@ -427,8 +477,11 @@ class RecceStateLoader:
|
|
|
427
477
|
return RecceState.from_file(tmp.name, file_type=SupportedFileTypes.GZIP)
|
|
428
478
|
|
|
429
479
|
def _export_state_to_cloud(self) -> Tuple[Union[str, None], str]:
|
|
430
|
-
if
|
|
431
|
-
|
|
480
|
+
if self.catalog == "github":
|
|
481
|
+
if (self.pr_info is None) or (self.pr_info.id is None) or (self.pr_info.repository is None):
|
|
482
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
483
|
+
elif self.catalog == "preview":
|
|
484
|
+
pass
|
|
432
485
|
|
|
433
486
|
check_status = CheckDAO().status()
|
|
434
487
|
metadata = {
|
|
@@ -443,9 +496,9 @@ class RecceStateLoader:
|
|
|
443
496
|
logger.info("Store recce state to Recce Cloud")
|
|
444
497
|
message = self._export_state_to_recce_cloud(metadata=metadata)
|
|
445
498
|
metadata = self._get_metadata_from_recce_cloud()
|
|
446
|
-
if metadata
|
|
447
|
-
|
|
448
|
-
|
|
499
|
+
state_etag = metadata.get("etag") if metadata else None
|
|
500
|
+
if message:
|
|
501
|
+
logger.warning(message)
|
|
449
502
|
return message, state_etag
|
|
450
503
|
|
|
451
504
|
def _export_state_to_recce_cloud(self, metadata: dict = None) -> Union[str, None]:
|
|
@@ -453,24 +506,42 @@ class RecceStateLoader:
|
|
|
453
506
|
|
|
454
507
|
import requests
|
|
455
508
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
509
|
+
if self.catalog == "github":
|
|
510
|
+
presigned_url = RecceCloud(token=self.token).get_presigned_url_by_github_repo(
|
|
511
|
+
method=PresignedUrlMethod.UPLOAD,
|
|
512
|
+
repository=self.pr_info.repository,
|
|
513
|
+
artifact_name=RECCE_STATE_COMPRESSED_FILE,
|
|
514
|
+
pr_id=self.pr_info.id,
|
|
515
|
+
metadata=metadata,
|
|
516
|
+
)
|
|
517
|
+
elif self.catalog == "preview":
|
|
518
|
+
share_id = self.cloud_options.get("share_id")
|
|
519
|
+
presigned_url = RecceCloud(token=self.token).get_presigned_url_by_share_id(
|
|
520
|
+
method=PresignedUrlMethod.UPLOAD,
|
|
521
|
+
share_id=share_id,
|
|
522
|
+
metadata=metadata,
|
|
523
|
+
)
|
|
463
524
|
compress_passwd = self.cloud_options.get("password")
|
|
464
|
-
|
|
525
|
+
if compress_passwd:
|
|
526
|
+
headers = s3_sse_c_headers(compress_passwd)
|
|
527
|
+
else:
|
|
528
|
+
headers = {}
|
|
529
|
+
|
|
465
530
|
if metadata:
|
|
466
531
|
headers["x-amz-tagging"] = urlencode(metadata)
|
|
467
532
|
with tempfile.NamedTemporaryFile() as tmp:
|
|
468
|
-
self.
|
|
469
|
-
|
|
470
|
-
|
|
533
|
+
if self.catalog == "github":
|
|
534
|
+
file_type = SupportedFileTypes.GZIP
|
|
535
|
+
elif self.catalog == "preview":
|
|
536
|
+
file_type = SupportedFileTypes.FILE
|
|
537
|
+
self._export_state_to_file(tmp.name, file_type=file_type)
|
|
538
|
+
|
|
539
|
+
with open(tmp.name, "rb") as fd:
|
|
540
|
+
response = requests.put(presigned_url, data=fd.read(), headers=headers)
|
|
541
|
+
if response.status_code not in [200, 204]:
|
|
471
542
|
self.error_message = response.text
|
|
472
543
|
return "Failed to upload the state file to Recce Cloud. Reason: " + response.text
|
|
473
|
-
return
|
|
544
|
+
return None
|
|
474
545
|
|
|
475
546
|
def _export_state_to_s3_bucket(self, metadata: dict = None) -> Union[str, None]:
|
|
476
547
|
import tempfile
|
|
@@ -479,11 +550,18 @@ class RecceStateLoader:
|
|
|
479
550
|
|
|
480
551
|
s3_client = boto3.client("s3")
|
|
481
552
|
s3_bucket_name = self.cloud_options.get("host").replace("s3://", "")
|
|
482
|
-
|
|
553
|
+
if self.catalog == "github":
|
|
554
|
+
s3_bucket_key = (
|
|
555
|
+
f"{self.catalog}/{self.pr_info.repository}/pulls/{self.pr_info.id}/{RECCE_STATE_COMPRESSED_FILE}"
|
|
556
|
+
)
|
|
557
|
+
elif self.catalog == "preview":
|
|
558
|
+
s3_bucket_key = f"{self.catalog}/{self.share_id}/{RECCE_STATE_FILE}"
|
|
559
|
+
else:
|
|
560
|
+
raise RecceException(f"Unsupported catalog type. {self.catalog} is not supported.")
|
|
483
561
|
|
|
484
562
|
rc, error_message = check_s3_bucket(s3_bucket_name)
|
|
485
563
|
if rc is False:
|
|
486
|
-
raise
|
|
564
|
+
raise RecceException(error_message)
|
|
487
565
|
|
|
488
566
|
with tempfile.NamedTemporaryFile() as tmp:
|
|
489
567
|
self._export_state_to_file(tmp.name, file_type=SupportedFileTypes.GZIP)
|
|
@@ -495,7 +573,7 @@ class RecceStateLoader:
|
|
|
495
573
|
# Casting all the values under metadata to string
|
|
496
574
|
ExtraArgs={"Metadata": {k: str(v) for k, v in metadata.items()}},
|
|
497
575
|
)
|
|
498
|
-
RecceCloud(token=self.
|
|
576
|
+
RecceCloud(token=self.token).update_github_pull_request_check(self.pr_info, metadata)
|
|
499
577
|
return f"The state file is uploaded to ' s3://{s3_bucket_name}/{s3_bucket_key}'"
|
|
500
578
|
|
|
501
579
|
def _get_artifact_metadata_from_s3_bucket(self, artifact_name: str) -> Union[dict, None]:
|
|
@@ -510,7 +588,7 @@ class RecceStateLoader:
|
|
|
510
588
|
return metadata
|
|
511
589
|
except botocore.exceptions.ClientError as e:
|
|
512
590
|
self.error_message = e.response.get("Error", {}).get("Message")
|
|
513
|
-
raise
|
|
591
|
+
raise RecceException("Failed to get artifact metadata from Recce Cloud.")
|
|
514
592
|
|
|
515
593
|
def _export_state_to_file(
|
|
516
594
|
self, file_path: Optional[str] = None, file_type: SupportedFileTypes = SupportedFileTypes.FILE
|
|
@@ -538,15 +616,16 @@ class RecceCloudStateManager:
|
|
|
538
616
|
self.pr_info = None
|
|
539
617
|
self.error_message = None
|
|
540
618
|
self.hint_message = None
|
|
619
|
+
self.github_token = self.cloud_options.get("github_token")
|
|
541
620
|
|
|
542
|
-
if not self.
|
|
543
|
-
raise
|
|
544
|
-
self.pr_info = fetch_pr_metadata(cloud=True, github_token=self.
|
|
621
|
+
if not self.github_token:
|
|
622
|
+
raise RecceException(RECCE_CLOUD_TOKEN_MISSING.error_message)
|
|
623
|
+
self.pr_info = fetch_pr_metadata(cloud=True, github_token=self.github_token)
|
|
545
624
|
if self.pr_info.id is None:
|
|
546
|
-
raise
|
|
625
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
547
626
|
|
|
548
627
|
def verify(self) -> bool:
|
|
549
|
-
if self.
|
|
628
|
+
if self.github_token is None:
|
|
550
629
|
self.error_message = RECCE_CLOUD_TOKEN_MISSING.error_message
|
|
551
630
|
self.hint_message = RECCE_CLOUD_TOKEN_MISSING.hint_message
|
|
552
631
|
return False
|
|
@@ -561,7 +640,7 @@ class RecceCloudStateManager:
|
|
|
561
640
|
return self.error_message, self.hint_message
|
|
562
641
|
|
|
563
642
|
def _check_state_in_recce_cloud(self) -> bool:
|
|
564
|
-
return RecceCloud(token=self.
|
|
643
|
+
return RecceCloud(token=self.github_token).check_artifacts_exists(self.pr_info)
|
|
565
644
|
|
|
566
645
|
def _check_state_in_s3_bucket(self) -> bool:
|
|
567
646
|
import boto3
|
|
@@ -588,7 +667,7 @@ class RecceCloudStateManager:
|
|
|
588
667
|
|
|
589
668
|
import requests
|
|
590
669
|
|
|
591
|
-
presigned_url = RecceCloud(token=self.
|
|
670
|
+
presigned_url = RecceCloud(token=self.github_token).get_presigned_url_by_github_repo(
|
|
592
671
|
method=PresignedUrlMethod.UPLOAD,
|
|
593
672
|
repository=self.pr_info.repository,
|
|
594
673
|
artifact_name=RECCE_STATE_COMPRESSED_FILE,
|
|
@@ -616,7 +695,7 @@ class RecceCloudStateManager:
|
|
|
616
695
|
|
|
617
696
|
rc, error_message = check_s3_bucket(s3_bucket_name)
|
|
618
697
|
if rc is False:
|
|
619
|
-
raise
|
|
698
|
+
raise RecceException(error_message)
|
|
620
699
|
|
|
621
700
|
with tempfile.NamedTemporaryFile() as tmp:
|
|
622
701
|
state.to_file(tmp.name, file_type=SupportedFileTypes.GZIP)
|
|
@@ -628,12 +707,12 @@ class RecceCloudStateManager:
|
|
|
628
707
|
# Casting all the values under metadata to string
|
|
629
708
|
ExtraArgs={"Metadata": {k: str(v) for k, v in metadata.items()}},
|
|
630
709
|
)
|
|
631
|
-
RecceCloud(token=self.
|
|
710
|
+
RecceCloud(token=self.github_token).update_github_pull_request_check(self.pr_info, metadata)
|
|
632
711
|
return f"The state file is uploaded to ' s3://{s3_bucket_name}/{s3_bucket_key}'"
|
|
633
712
|
|
|
634
713
|
def upload_state_to_cloud(self, state: RecceState) -> Union[str, None]:
|
|
635
714
|
if (self.pr_info is None) or (self.pr_info.id is None) or (self.pr_info.repository is None):
|
|
636
|
-
raise
|
|
715
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
637
716
|
|
|
638
717
|
checks = state.checks
|
|
639
718
|
|
|
@@ -658,7 +737,7 @@ class RecceCloudStateManager:
|
|
|
658
737
|
|
|
659
738
|
rc, error_message = check_s3_bucket(s3_bucket_name)
|
|
660
739
|
if rc is False:
|
|
661
|
-
raise
|
|
740
|
+
raise RecceException(error_message)
|
|
662
741
|
|
|
663
742
|
response = s3_client.get_object(Bucket=s3_bucket_name, Key=s3_bucket_key)
|
|
664
743
|
byte_stream = io.BytesIO(response["Body"].read())
|
|
@@ -676,7 +755,7 @@ class RecceCloudStateManager:
|
|
|
676
755
|
|
|
677
756
|
import requests
|
|
678
757
|
|
|
679
|
-
presigned_url = RecceCloud(token=self.
|
|
758
|
+
presigned_url = RecceCloud(token=self.github_token).get_presigned_url_by_github_repo(
|
|
680
759
|
method=PresignedUrlMethod.DOWNLOAD,
|
|
681
760
|
repository=self.pr_info.repository,
|
|
682
761
|
artifact_name=RECCE_STATE_COMPRESSED_FILE,
|
|
@@ -685,13 +764,13 @@ class RecceCloudStateManager:
|
|
|
685
764
|
|
|
686
765
|
password = self.cloud_options.get("password")
|
|
687
766
|
if password is None:
|
|
688
|
-
raise
|
|
767
|
+
raise RecceException(RECCE_CLOUD_PASSWORD_MISSING.error_message)
|
|
689
768
|
|
|
690
769
|
headers = s3_sse_c_headers(password)
|
|
691
770
|
response = requests.get(presigned_url, headers=headers)
|
|
692
771
|
|
|
693
772
|
if response.status_code != 200:
|
|
694
|
-
raise
|
|
773
|
+
raise RecceException(
|
|
695
774
|
f"{response.status_code} Failed to download the state file from Recce Cloud. The password could be wrong."
|
|
696
775
|
)
|
|
697
776
|
|
|
@@ -707,7 +786,7 @@ class RecceCloudStateManager:
|
|
|
707
786
|
|
|
708
787
|
def download_state_from_cloud(self, filepath: str) -> Union[str, None]:
|
|
709
788
|
if (self.pr_info is None) or (self.pr_info.id is None) or (self.pr_info.repository is None):
|
|
710
|
-
raise
|
|
789
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
711
790
|
|
|
712
791
|
if self.cloud_options.get("host", "").startswith("s3://"):
|
|
713
792
|
logger.debug("Download state file from AWS S3 bucket...")
|
|
@@ -738,12 +817,12 @@ class RecceCloudStateManager:
|
|
|
738
817
|
delete_response = s3_client.delete_objects(Bucket=s3_bucket_name, Delete={"Objects": delete_objects})
|
|
739
818
|
if "Deleted" not in delete_response:
|
|
740
819
|
return False, "Failed to delete the state file from the S3 bucket."
|
|
741
|
-
RecceCloud(token=self.
|
|
820
|
+
RecceCloud(token=self.github_token).update_github_pull_request_check(self.pr_info)
|
|
742
821
|
return True, None
|
|
743
822
|
|
|
744
823
|
def _purge_state_from_recce_cloud(self) -> (bool, str):
|
|
745
824
|
try:
|
|
746
|
-
RecceCloud(token=self.
|
|
825
|
+
RecceCloud(token=self.github_token).purge_artifacts(self.pr_info)
|
|
747
826
|
except RecceCloudException as e:
|
|
748
827
|
return False, e.reason
|
|
749
828
|
return True, None
|
recce/util/api_token.py
CHANGED
|
@@ -17,7 +17,9 @@ def show_invalid_api_token_message():
|
|
|
17
17
|
Show the message when the API token is invalid.
|
|
18
18
|
"""
|
|
19
19
|
console.print("[[red]Error[/red]] Invalid Recce Cloud API token.")
|
|
20
|
-
console.print(
|
|
20
|
+
console.print("Please associate with your Recce Cloud account by the following command 'recce connect-to-cloud'.")
|
|
21
|
+
console.print(
|
|
22
|
+
"For more information, please visit: https://docs.reccehq.com/recce-cloud/share-recce-session-securely/#configure-recce-cloud-association-manually")
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
def prepare_api_token(
|
recce/util/recce_cloud.py
CHANGED
|
@@ -66,7 +66,7 @@ class RecceCloud:
|
|
|
66
66
|
pass
|
|
67
67
|
return False
|
|
68
68
|
|
|
69
|
-
def
|
|
69
|
+
def get_presigned_url_by_github_repo(
|
|
70
70
|
self,
|
|
71
71
|
method: PresignedUrlMethod,
|
|
72
72
|
repository: str,
|
|
@@ -78,7 +78,16 @@ class RecceCloud:
|
|
|
78
78
|
response = self._fetch_presigned_url(method, repository, artifact_name, metadata, pr_id, branch)
|
|
79
79
|
return response.get("presigned_url")
|
|
80
80
|
|
|
81
|
-
def
|
|
81
|
+
def get_presigned_url_by_share_id(
|
|
82
|
+
self,
|
|
83
|
+
method: PresignedUrlMethod,
|
|
84
|
+
share_id: str,
|
|
85
|
+
metadata: dict = None,
|
|
86
|
+
) -> str:
|
|
87
|
+
response = self._fetch_presigned_url_by_share_id(method, share_id, metadata=metadata)
|
|
88
|
+
return response.get("presigned_url")
|
|
89
|
+
|
|
90
|
+
def get_download_presigned_url_by_github_repo_with_tags(
|
|
82
91
|
self, repository: str, artifact_name: str, branch: str = None
|
|
83
92
|
) -> (str, dict):
|
|
84
93
|
response = self._fetch_presigned_url(PresignedUrlMethod.DOWNLOAD, repository, artifact_name, branch=branch)
|
|
@@ -110,6 +119,33 @@ class RecceCloud:
|
|
|
110
119
|
)
|
|
111
120
|
return response.json()
|
|
112
121
|
|
|
122
|
+
def _fetch_presigned_url_by_share_id(
|
|
123
|
+
self,
|
|
124
|
+
method: PresignedUrlMethod,
|
|
125
|
+
share_id: str,
|
|
126
|
+
metadata: dict = None,
|
|
127
|
+
):
|
|
128
|
+
api_url = f"{self.base_url}/shares/{share_id}/presigned/{method}"
|
|
129
|
+
data = None
|
|
130
|
+
# Only provide metadata for upload requests
|
|
131
|
+
if method == PresignedUrlMethod.UPLOAD:
|
|
132
|
+
# Covert metadata values to strings to ensure JSON serializability
|
|
133
|
+
data = {"metadata": {key: str(value) for key, value in metadata.items()}} if metadata else None
|
|
134
|
+
response = self._request(
|
|
135
|
+
"POST",
|
|
136
|
+
api_url,
|
|
137
|
+
json=data,
|
|
138
|
+
)
|
|
139
|
+
if response.status_code != 200:
|
|
140
|
+
raise RecceCloudException(
|
|
141
|
+
message="Failed to {method} artifact {preposition} Recce Cloud.".format(
|
|
142
|
+
method=method, preposition="from" if method == PresignedUrlMethod.DOWNLOAD else "to"
|
|
143
|
+
),
|
|
144
|
+
reason=response.text,
|
|
145
|
+
status_code=response.status_code,
|
|
146
|
+
)
|
|
147
|
+
return response.json()
|
|
148
|
+
|
|
113
149
|
def get_artifact_metadata(self, pr_info: PullRequestInfo) -> dict:
|
|
114
150
|
api_url = f"{self.base_url}/{pr_info.repository}/pulls/{pr_info.id}/metadata"
|
|
115
151
|
response = self._request("GET", api_url)
|