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.
- recce/VERSION +1 -1
- recce/__init__.py +27 -22
- recce/adapter/base.py +11 -14
- recce/adapter/dbt_adapter/__init__.py +845 -461
- recce/adapter/dbt_adapter/dbt_version.py +3 -0
- recce/adapter/sqlmesh_adapter.py +24 -35
- recce/apis/check_api.py +59 -42
- recce/apis/check_events_api.py +353 -0
- recce/apis/check_func.py +41 -35
- recce/apis/run_api.py +25 -19
- recce/apis/run_func.py +64 -25
- recce/artifact.py +119 -51
- recce/cli.py +1301 -324
- recce/config.py +43 -34
- recce/connect_to_cloud.py +138 -0
- recce/core.py +55 -47
- recce/data/404/index.html +2 -0
- recce/data/404.html +2 -1
- recce/data/__next.@lineage.!KHNsb3Qp.__PAGE__.txt +7 -0
- recce/data/__next.@lineage.!KHNsb3Qp.txt +4 -0
- recce/data/__next.__PAGE__.txt +6 -0
- recce/data/__next._full.txt +32 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +14 -0
- recce/data/__next._tree.txt +8 -0
- recce/data/_next/static/chunks/025a7e3e3f9f40ae.js +1 -0
- recce/data/_next/static/chunks/0ce56d67ef5779ca.js +4 -0
- recce/data/_next/static/chunks/1a6a78780155dac7.js +48 -0
- recce/data/_next/static/chunks/1de8485918b9182a.css +2 -0
- recce/data/_next/static/chunks/1e4b1b50d1e34993.js +1 -0
- recce/data/_next/static/chunks/206d5d181e4c738e.js +1 -0
- recce/data/_next/static/chunks/2c357efc34c5b859.js +25 -0
- recce/data/_next/static/chunks/2e9d95d2d48c479c.js +1 -0
- recce/data/_next/static/chunks/2f016dc4a3edad2e.js +2 -0
- recce/data/_next/static/chunks/313251962d698f7c.js +1 -0
- recce/data/_next/static/chunks/3a9f021f38eb5574.css +1 -0
- recce/data/_next/static/chunks/40079da8d2b8f651.js +1 -0
- recce/data/_next/static/chunks/4599182bffb64661.js +38 -0
- recce/data/_next/static/chunks/4e62f6e184173580.js +1 -0
- recce/data/_next/static/chunks/5c4dfb0d09eaa401.js +1 -0
- recce/data/_next/static/chunks/69e4f06ccfdfc3ac.js +1 -0
- recce/data/_next/static/chunks/6b206cb4707d6bee.js +1 -0
- recce/data/_next/static/chunks/6d8557f062aa4386.css +1 -0
- recce/data/_next/static/chunks/7fbe3650bd83b6b5.js +1 -0
- recce/data/_next/static/chunks/83fa823a825674f6.js +1 -0
- recce/data/_next/static/chunks/848a6c9b5f55f7ed.js +1 -0
- recce/data/_next/static/chunks/859462b0858aef88.css +2 -0
- recce/data/_next/static/chunks/923964f18c87d0f1.css +1 -0
- recce/data/_next/static/chunks/939390f911895d7c.js +48 -0
- recce/data/_next/static/chunks/99a9817237a07f43.js +1 -0
- recce/data/_next/static/chunks/9fed8b4b2b924054.js +5 -0
- recce/data/_next/static/chunks/b6949f6c5892110c.js +1 -0
- recce/data/_next/static/chunks/b851a1d3f8149828.js +1 -0
- recce/data/_next/static/chunks/c734f9ad957de0b4.js +1 -0
- recce/data/_next/static/chunks/cdde321b0ec75717.js +2 -0
- recce/data/_next/static/chunks/d0f91117d77ff844.css +1 -0
- recce/data/_next/static/chunks/d6c8667911c2500f.js +1 -0
- recce/data/_next/static/chunks/da8dab68c02752cf.js +74 -0
- recce/data/_next/static/chunks/dc074049c9d12d97.js +109 -0
- recce/data/_next/static/chunks/ee7f1a8227342421.js +1 -0
- recce/data/_next/static/chunks/fa2f4e56c2fccc73.js +1 -0
- recce/data/_next/static/chunks/turbopack-1fad664f62979b93.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.f9d58125.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/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.b671449b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_buildManifest.js +11 -0
- recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_clientMiddlewareManifest.json +1 -0
- recce/data/_not-found/__next._full.txt +24 -0
- recce/data/_not-found/__next._head.txt +8 -0
- recce/data/_not-found/__next._index.txt +13 -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 +6 -0
- recce/data/_not-found/index.html +2 -0
- recce/data/_not-found/index.txt +24 -0
- recce/data/auth_callback.html +68 -0
- recce/data/checks/__next.@lineage.__DEFAULT__.txt +7 -0
- recce/data/checks/__next._full.txt +39 -0
- recce/data/checks/__next._head.txt +8 -0
- recce/data/checks/__next._index.txt +14 -0
- recce/data/checks/__next._tree.txt +8 -0
- recce/data/checks/__next.checks.__PAGE__.txt +10 -0
- recce/data/checks/__next.checks.txt +4 -0
- recce/data/checks/index.html +2 -0
- recce/data/checks/index.txt +39 -0
- recce/data/imgs/reload-image.svg +4 -0
- recce/data/index.html +2 -27
- recce/data/index.txt +32 -7
- recce/data/lineage/__next.@lineage.__DEFAULT__.txt +7 -0
- recce/data/lineage/__next._full.txt +39 -0
- recce/data/lineage/__next._head.txt +8 -0
- recce/data/lineage/__next._index.txt +14 -0
- recce/data/lineage/__next._tree.txt +8 -0
- recce/data/lineage/__next.lineage.__PAGE__.txt +10 -0
- recce/data/lineage/__next.lineage.txt +4 -0
- recce/data/lineage/index.html +2 -0
- recce/data/lineage/index.txt +39 -0
- recce/data/query/__next.@lineage.__DEFAULT__.txt +7 -0
- recce/data/query/__next._full.txt +37 -0
- recce/data/query/__next._head.txt +8 -0
- recce/data/query/__next._index.txt +14 -0
- recce/data/query/__next._tree.txt +8 -0
- recce/data/query/__next.query.__PAGE__.txt +9 -0
- recce/data/query/__next.query.txt +4 -0
- recce/data/query/index.html +2 -0
- recce/data/query/index.txt +37 -0
- recce/diff.py +6 -12
- recce/event/CONFIG.bak +1 -0
- recce/event/__init__.py +86 -74
- recce/event/collector.py +33 -22
- recce/event/track.py +49 -27
- recce/exceptions.py +1 -1
- recce/git.py +7 -7
- recce/github.py +57 -53
- recce/mcp_server.py +725 -0
- recce/models/__init__.py +4 -1
- recce/models/check.py +438 -21
- recce/models/run.py +1 -0
- recce/models/types.py +134 -28
- recce/pull_request.py +27 -25
- recce/run.py +179 -122
- recce/server.py +394 -104
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +644 -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 +196 -149
- recce/tasks/__init__.py +19 -3
- recce/tasks/core.py +11 -13
- recce/tasks/dataframe.py +82 -18
- recce/tasks/histogram.py +69 -34
- recce/tasks/lineage.py +2 -2
- recce/tasks/profile.py +152 -86
- recce/tasks/query.py +180 -89
- recce/tasks/rowcount.py +37 -31
- recce/tasks/schema.py +18 -15
- recce/tasks/top_k.py +35 -35
- recce/tasks/utils.py +147 -0
- recce/tasks/valuediff.py +247 -155
- recce/util/__init__.py +3 -0
- recce/util/api_token.py +80 -0
- recce/util/breaking.py +105 -100
- recce/util/cll.py +274 -219
- recce/util/cloud/__init__.py +15 -0
- recce/util/cloud/base.py +115 -0
- recce/util/cloud/check_events.py +190 -0
- recce/util/cloud/checks.py +242 -0
- recce/util/io.py +22 -17
- recce/util/lineage.py +65 -16
- recce/util/logger.py +1 -1
- recce/util/onboarding_state.py +45 -0
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +347 -72
- recce/util/singleton.py +4 -4
- recce/util/startup_perf.py +121 -0
- recce/yaml/__init__.py +7 -10
- recce_nightly-1.30.0.20251221.dist-info/METADATA +195 -0
- recce_nightly-1.30.0.20251221.dist-info/RECORD +183 -0
- {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/WHEEL +1 -2
- recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
- recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
- recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
- recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
- recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
- recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
- recce/data/_next/static/chunks/500-e51c92a025a51234.js +0 -65
- recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
- recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
- recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
- recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
- recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
- recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
- recce/data/_next/static/chunks/app/page-9adc25782272ed2e.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
- recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
- recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
- recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
- recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
- recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
- recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
- recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
- recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
- recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
- recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +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-800-normal.97e20d5e.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/data/_next/static/qiyFlux77VkhxiceAJe_F/_buildManifest.js +0 -1
- recce/state.py +0 -753
- recce_nightly-0.62.0.20250417.dist-info/METADATA +0 -311
- recce_nightly-0.62.0.20250417.dist-info/RECORD +0 -139
- recce_nightly-0.62.0.20250417.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- tests/adapter/__init__.py +0 -0
- tests/adapter/dbt_adapter/__init__.py +0 -0
- tests/adapter/dbt_adapter/conftest.py +0 -13
- tests/adapter/dbt_adapter/dbt_test_helper.py +0 -283
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -40
- tests/adapter/dbt_adapter/test_dbt_cll.py +0 -102
- tests/adapter/dbt_adapter/test_selector.py +0 -177
- tests/tasks/__init__.py +0 -0
- tests/tasks/conftest.py +0 -4
- tests/tasks/test_histogram.py +0 -137
- tests/tasks/test_lineage.py +0 -42
- tests/tasks/test_preset_checks.py +0 -50
- tests/tasks/test_profile.py +0 -73
- tests/tasks/test_query.py +0 -151
- tests/tasks/test_row_count.py +0 -116
- tests/tasks/test_schema.py +0 -99
- tests/tasks/test_top_k.py +0 -73
- tests/tasks/test_valuediff.py +0 -74
- tests/test_cli.py +0 -122
- tests/test_config.py +0 -45
- tests/test_core.py +0 -27
- tests/test_dbt.py +0 -36
- tests/test_pull_request.py +0 -130
- tests/test_server.py +0 -98
- tests/test_state.py +0 -123
- tests/test_summary.py +0 -57
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- /recce/data/_next/static/{qiyFlux77VkhxiceAJe_F → nX-Uz0AH6Tc6hIQUFGqaB}/_ssgManifest.js +0 -0
- {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/entry_points.txt +0 -0
- {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/licenses/LICENSE +0 -0
recce/state/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .cloud import (
|
|
2
|
+
CloudStateLoader,
|
|
3
|
+
RecceCloudStateManager,
|
|
4
|
+
RecceShareStateManager,
|
|
5
|
+
s3_sse_c_headers,
|
|
6
|
+
)
|
|
7
|
+
from .const import ErrorMessage
|
|
8
|
+
from .local import FileStateLoader
|
|
9
|
+
from .state import (
|
|
10
|
+
ArtifactsRoot,
|
|
11
|
+
GitRepoInfo,
|
|
12
|
+
PullRequestInfo,
|
|
13
|
+
RecceState,
|
|
14
|
+
RecceStateMetadata,
|
|
15
|
+
)
|
|
16
|
+
from .state_loader import RecceStateLoader
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ArtifactsRoot",
|
|
20
|
+
"ErrorMessage",
|
|
21
|
+
"RecceCloudStateManager",
|
|
22
|
+
"RecceShareStateManager",
|
|
23
|
+
"RecceState",
|
|
24
|
+
"RecceStateLoader",
|
|
25
|
+
"CloudStateLoader",
|
|
26
|
+
"FileStateLoader",
|
|
27
|
+
"RecceStateMetadata",
|
|
28
|
+
"s3_sse_c_headers",
|
|
29
|
+
"GitRepoInfo",
|
|
30
|
+
"PullRequestInfo",
|
|
31
|
+
]
|
recce/state/cloud.py
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from base64 import b64encode
|
|
4
|
+
from hashlib import md5, sha256
|
|
5
|
+
from typing import Dict, Optional, Tuple, Union
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
from recce.exceptions import RecceException
|
|
9
|
+
from recce.pull_request import PullRequestInfo, fetch_pr_metadata
|
|
10
|
+
from recce.util.io import SupportedFileTypes, file_io_factory
|
|
11
|
+
from recce.util.recce_cloud import PresignedUrlMethod, RecceCloud, RecceCloudException
|
|
12
|
+
from recce.util.startup_perf import track_timing
|
|
13
|
+
|
|
14
|
+
from ..event import get_recce_api_token
|
|
15
|
+
from ..models import CheckDAO
|
|
16
|
+
from .const import (
|
|
17
|
+
RECCE_API_TOKEN_MISSING,
|
|
18
|
+
RECCE_CLOUD_PASSWORD_MISSING,
|
|
19
|
+
RECCE_CLOUD_TOKEN_MISSING,
|
|
20
|
+
RECCE_STATE_COMPRESSED_FILE,
|
|
21
|
+
)
|
|
22
|
+
from .state import RecceState
|
|
23
|
+
from .state_loader import RecceStateLoader
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("uvicorn")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def s3_sse_c_headers(password: str) -> Dict[str, str]:
|
|
29
|
+
hashed_password = sha256()
|
|
30
|
+
md5_hash = md5()
|
|
31
|
+
hashed_password.update(password.encode())
|
|
32
|
+
md5_hash.update(hashed_password.digest())
|
|
33
|
+
encoded_passwd = b64encode(hashed_password.digest()).decode("utf-8")
|
|
34
|
+
encoded_md5 = b64encode(md5_hash.digest()).decode("utf-8")
|
|
35
|
+
return {
|
|
36
|
+
"x-amz-server-side-encryption-customer-algorithm": "AES256",
|
|
37
|
+
"x-amz-server-side-encryption-customer-key": encoded_passwd,
|
|
38
|
+
"x-amz-server-side-encryption-customer-key-MD5": encoded_md5,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CloudStateLoader(RecceStateLoader):
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
review_mode: bool = False,
|
|
46
|
+
cloud_options: Optional[Dict[str, str]] = None,
|
|
47
|
+
initial_state: Optional[RecceState] = None,
|
|
48
|
+
):
|
|
49
|
+
super().__init__(
|
|
50
|
+
cloud_mode=True,
|
|
51
|
+
review_mode=review_mode,
|
|
52
|
+
cloud_options=cloud_options,
|
|
53
|
+
initial_state=initial_state,
|
|
54
|
+
)
|
|
55
|
+
self.recce_cloud = RecceCloud(token=self.token)
|
|
56
|
+
# Initialize org_id and project_id attributes
|
|
57
|
+
# These will be set when loading from session
|
|
58
|
+
self.org_id = None
|
|
59
|
+
self.project_id = None
|
|
60
|
+
|
|
61
|
+
def verify(self) -> bool:
|
|
62
|
+
if self.catalog == "github":
|
|
63
|
+
if self.cloud_options.get("github_token") is None:
|
|
64
|
+
self.error_message = RECCE_CLOUD_TOKEN_MISSING.error_message
|
|
65
|
+
self.hint_message = RECCE_CLOUD_TOKEN_MISSING.hint_message
|
|
66
|
+
return False
|
|
67
|
+
if not self.cloud_options.get("host"):
|
|
68
|
+
if self.cloud_options.get("password") is None:
|
|
69
|
+
self.error_message = RECCE_CLOUD_PASSWORD_MISSING.error_message
|
|
70
|
+
self.hint_message = RECCE_CLOUD_PASSWORD_MISSING.hint_message
|
|
71
|
+
return False
|
|
72
|
+
elif self.catalog == "preview":
|
|
73
|
+
if self.cloud_options.get("api_token") is None:
|
|
74
|
+
self.error_message = RECCE_API_TOKEN_MISSING.error_message
|
|
75
|
+
self.hint_message = RECCE_API_TOKEN_MISSING.hint_message
|
|
76
|
+
return False
|
|
77
|
+
if self.cloud_options.get("share_id") is None:
|
|
78
|
+
self.error_message = "No share ID is provided for the preview catalog."
|
|
79
|
+
self.hint_message = (
|
|
80
|
+
'Please provide a share URL in the command argument with option "--share-url <share-url>"'
|
|
81
|
+
)
|
|
82
|
+
return False
|
|
83
|
+
elif self.catalog == "session":
|
|
84
|
+
if self.cloud_options.get("api_token") is None:
|
|
85
|
+
self.error_message = RECCE_API_TOKEN_MISSING.error_message
|
|
86
|
+
self.hint_message = RECCE_API_TOKEN_MISSING.hint_message
|
|
87
|
+
return False
|
|
88
|
+
if self.cloud_options.get("session_id") is None:
|
|
89
|
+
self.error_message = "No session ID is provided for the session catalog."
|
|
90
|
+
self.hint_message = (
|
|
91
|
+
'Please provide a session ID in the command argument with option "--session-id <session-id>"'
|
|
92
|
+
)
|
|
93
|
+
return False
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
def purge(self) -> bool:
|
|
97
|
+
rc, err_msg = RecceCloudStateManager(self.cloud_options).purge_cloud_state()
|
|
98
|
+
if err_msg:
|
|
99
|
+
self.error_message = err_msg
|
|
100
|
+
return rc
|
|
101
|
+
|
|
102
|
+
def _load_state(self) -> Tuple[RecceState, str]:
|
|
103
|
+
"""
|
|
104
|
+
Load the state from Recce Cloud based on catalog type.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
RecceState: The state object.
|
|
108
|
+
str: The etag of the state file (only used for GitHub).
|
|
109
|
+
"""
|
|
110
|
+
if self.catalog == "github":
|
|
111
|
+
return self._load_state_from_github()
|
|
112
|
+
elif self.catalog == "preview":
|
|
113
|
+
return self._load_state_from_preview()
|
|
114
|
+
elif self.catalog == "session":
|
|
115
|
+
return self._load_state_from_session(), None
|
|
116
|
+
else:
|
|
117
|
+
raise RecceException(f"Unsupported catalog type: {self.catalog}")
|
|
118
|
+
|
|
119
|
+
def _load_state_from_github(self) -> Tuple[RecceState, str]:
|
|
120
|
+
"""Load state from GitHub PR with etag checking."""
|
|
121
|
+
if (self.pr_info is None) or (self.pr_info.id is None) or (self.pr_info.repository is None):
|
|
122
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
123
|
+
|
|
124
|
+
logger.debug("Fetching GitHub state from Recce Cloud...")
|
|
125
|
+
|
|
126
|
+
# Check metadata and etag for GitHub only
|
|
127
|
+
metadata = self._get_metadata_from_recce_cloud()
|
|
128
|
+
state_etag = metadata.get("etag") if metadata else None
|
|
129
|
+
|
|
130
|
+
# Return cached state if etag matches
|
|
131
|
+
if self.state_etag and state_etag == self.state_etag:
|
|
132
|
+
return self.state, self.state_etag
|
|
133
|
+
|
|
134
|
+
# Download state from GitHub
|
|
135
|
+
presigned_url = self.recce_cloud.get_presigned_url_by_github_repo(
|
|
136
|
+
method=PresignedUrlMethod.DOWNLOAD,
|
|
137
|
+
pr_id=self.pr_info.id,
|
|
138
|
+
repository=self.pr_info.repository,
|
|
139
|
+
artifact_name=RECCE_STATE_COMPRESSED_FILE,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
password = self.cloud_options.get("password")
|
|
143
|
+
if password is None:
|
|
144
|
+
raise RecceException(RECCE_CLOUD_PASSWORD_MISSING.error_message)
|
|
145
|
+
|
|
146
|
+
headers = s3_sse_c_headers(password)
|
|
147
|
+
loaded_state = self._download_state_from_url(presigned_url, SupportedFileTypes.GZIP, headers)
|
|
148
|
+
|
|
149
|
+
# Handle the case where download returns None (404 error)
|
|
150
|
+
if loaded_state is None:
|
|
151
|
+
return None, state_etag
|
|
152
|
+
|
|
153
|
+
return loaded_state, state_etag
|
|
154
|
+
|
|
155
|
+
def _load_state_from_preview(self) -> Tuple[RecceState, None]:
|
|
156
|
+
"""Load state from preview share (no etag checking needed)."""
|
|
157
|
+
if self.share_id is None:
|
|
158
|
+
raise RecceException("Cannot load the share state from Recce Cloud. No share ID is provided.")
|
|
159
|
+
|
|
160
|
+
logger.debug("Fetching preview state from Recce Cloud...")
|
|
161
|
+
|
|
162
|
+
# Download state from preview share
|
|
163
|
+
presigned_url = self.recce_cloud.get_presigned_url_by_share_id(
|
|
164
|
+
method=PresignedUrlMethod.DOWNLOAD, share_id=self.share_id
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
loaded_state = self._download_state_from_url(presigned_url, SupportedFileTypes.FILE)
|
|
168
|
+
|
|
169
|
+
# Handle the case where download returns None (404 error)
|
|
170
|
+
if loaded_state is None:
|
|
171
|
+
return None, None
|
|
172
|
+
|
|
173
|
+
return loaded_state, None
|
|
174
|
+
|
|
175
|
+
def _get_metadata_from_recce_cloud(self) -> Union[dict, None]:
|
|
176
|
+
return self.recce_cloud.get_artifact_metadata(pr_info=self.pr_info) if self.pr_info else None
|
|
177
|
+
|
|
178
|
+
@track_timing("state_download")
|
|
179
|
+
def _download_state_from_url(
|
|
180
|
+
self, presigned_url: str, file_type: SupportedFileTypes, headers: dict = None
|
|
181
|
+
) -> RecceState:
|
|
182
|
+
"""Download state file from presigned URL and convert to RecceState."""
|
|
183
|
+
import tempfile
|
|
184
|
+
|
|
185
|
+
import requests
|
|
186
|
+
|
|
187
|
+
with tempfile.NamedTemporaryFile() as tmp:
|
|
188
|
+
response = requests.get(presigned_url, headers=headers)
|
|
189
|
+
|
|
190
|
+
if response.status_code == 404:
|
|
191
|
+
self.error_message = "The state file is not found in Recce Cloud."
|
|
192
|
+
return None
|
|
193
|
+
elif response.status_code != 200:
|
|
194
|
+
self.error_message = response.text
|
|
195
|
+
error_msg = f"{response.status_code} Failed to download the state file from Recce Cloud."
|
|
196
|
+
if headers: # GitHub case with password
|
|
197
|
+
error_msg += " The password could be wrong."
|
|
198
|
+
raise RecceException(error_msg)
|
|
199
|
+
|
|
200
|
+
with open(tmp.name, "wb") as f:
|
|
201
|
+
f.write(response.content)
|
|
202
|
+
|
|
203
|
+
return RecceState.from_file(tmp.name, file_type=file_type)
|
|
204
|
+
|
|
205
|
+
def _load_state_from_session(self) -> RecceState:
|
|
206
|
+
"""
|
|
207
|
+
Load state from session by:
|
|
208
|
+
1. Get session info
|
|
209
|
+
2. Download artifacts for both base and current sessions
|
|
210
|
+
3. Download recce_state if available, otherwise create empty state with artifacts
|
|
211
|
+
"""
|
|
212
|
+
if self.session_id is None:
|
|
213
|
+
raise RecceException("Cannot load the session state from Recce Cloud. No session ID is provided.")
|
|
214
|
+
|
|
215
|
+
# 1. Get session information
|
|
216
|
+
logger.debug(f"Getting session {self.session_id}")
|
|
217
|
+
session = self.recce_cloud.get_session(self.session_id)
|
|
218
|
+
|
|
219
|
+
pr_url = session.get("pr_link")
|
|
220
|
+
org_id = session.get("org_id")
|
|
221
|
+
project_id = session.get("project_id")
|
|
222
|
+
|
|
223
|
+
if not org_id or not project_id:
|
|
224
|
+
raise RecceException(f"Session {self.session_id} does not belong to a valid organization or project.")
|
|
225
|
+
|
|
226
|
+
# IMPORTANT: Store org_id and project_id as attributes for later use
|
|
227
|
+
# This allows CheckDAO and other components to access them without repeated API calls
|
|
228
|
+
self.org_id = org_id
|
|
229
|
+
self.project_id = project_id
|
|
230
|
+
|
|
231
|
+
# 2. Download manifests and catalogs for both session
|
|
232
|
+
logger.debug(f"Downloading current session artifacts for {self.session_id}")
|
|
233
|
+
current_artifacts = self._download_session_artifacts(self.recce_cloud, org_id, project_id, self.session_id)
|
|
234
|
+
|
|
235
|
+
logger.debug(f"Downloading base session artifacts for project {project_id}")
|
|
236
|
+
base_artifacts = self._download_base_session_artifacts(self.recce_cloud, org_id, project_id)
|
|
237
|
+
|
|
238
|
+
# 3. Try to download existing recce_state, otherwise create new state
|
|
239
|
+
try:
|
|
240
|
+
logger.debug(f"Downloading recce_state for session {self.session_id}")
|
|
241
|
+
state = self._download_session_recce_state(self.recce_cloud, org_id, project_id, self.session_id)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.debug(f"No existing recce_state found, creating new state: {e}")
|
|
244
|
+
state = RecceState()
|
|
245
|
+
|
|
246
|
+
if pr_url:
|
|
247
|
+
pr_id = pr_url.rstrip("/").split("/")[-1]
|
|
248
|
+
pull_request = PullRequestInfo(id=pr_id, url=pr_url)
|
|
249
|
+
self.pr_info = pull_request
|
|
250
|
+
if state.pull_request is None:
|
|
251
|
+
state.pull_request = pull_request
|
|
252
|
+
|
|
253
|
+
# Set artifacts regardless of whether we loaded existing state
|
|
254
|
+
state.artifacts.base = base_artifacts
|
|
255
|
+
state.artifacts.current = current_artifacts
|
|
256
|
+
state.checks = []
|
|
257
|
+
|
|
258
|
+
return state
|
|
259
|
+
|
|
260
|
+
def _download_session_artifacts(self, recce_cloud, org_id: str, project_id: str, session_id: str) -> dict:
|
|
261
|
+
"""Download manifest and catalog for a session, return JSON data directly."""
|
|
262
|
+
import requests
|
|
263
|
+
|
|
264
|
+
# Get download URLs
|
|
265
|
+
presigned_urls = recce_cloud.get_download_urls_by_session_id(org_id, project_id, session_id)
|
|
266
|
+
|
|
267
|
+
artifacts = {}
|
|
268
|
+
|
|
269
|
+
# Download manifest
|
|
270
|
+
response = requests.get(presigned_urls["manifest_url"])
|
|
271
|
+
if response.status_code == 200:
|
|
272
|
+
artifacts["manifest"] = response.json()
|
|
273
|
+
else:
|
|
274
|
+
raise RecceException(f"Failed to download manifest for session {session_id}")
|
|
275
|
+
|
|
276
|
+
# Download catalog
|
|
277
|
+
response = requests.get(presigned_urls["catalog_url"])
|
|
278
|
+
if response.status_code == 200:
|
|
279
|
+
artifacts["catalog"] = response.json()
|
|
280
|
+
else:
|
|
281
|
+
raise RecceException(f"Failed to download catalog for session {session_id}")
|
|
282
|
+
|
|
283
|
+
return artifacts
|
|
284
|
+
|
|
285
|
+
def _download_session_recce_state(self, recce_cloud, org_id: str, project_id: str, session_id: str) -> RecceState:
|
|
286
|
+
"""Download recce_state for a session."""
|
|
287
|
+
# Get download URLs (now includes recce_state_url)
|
|
288
|
+
presigned_urls = recce_cloud.get_download_urls_by_session_id(org_id, project_id, session_id)
|
|
289
|
+
recce_state_url = presigned_urls.get("recce_state_url")
|
|
290
|
+
|
|
291
|
+
if not recce_state_url:
|
|
292
|
+
raise RecceException(f"No recce_state_url found for session {session_id}")
|
|
293
|
+
|
|
294
|
+
# Reuse the existing download method
|
|
295
|
+
state = self._download_state_from_url(recce_state_url, SupportedFileTypes.FILE)
|
|
296
|
+
|
|
297
|
+
if state is None:
|
|
298
|
+
raise RecceException(f"Failed to download recce_state for session {session_id}")
|
|
299
|
+
|
|
300
|
+
return state
|
|
301
|
+
|
|
302
|
+
def _download_base_session_artifacts(self, recce_cloud, org_id: str, project_id: str) -> dict:
|
|
303
|
+
"""Download manifest and catalog for the base session, return JSON data directly."""
|
|
304
|
+
import requests
|
|
305
|
+
|
|
306
|
+
# Get download URLs for base session
|
|
307
|
+
presigned_urls = recce_cloud.get_base_session_download_urls(org_id, project_id)
|
|
308
|
+
|
|
309
|
+
artifacts = {}
|
|
310
|
+
|
|
311
|
+
# Download manifest
|
|
312
|
+
response = requests.get(presigned_urls["manifest_url"])
|
|
313
|
+
if response.status_code == 200:
|
|
314
|
+
artifacts["manifest"] = response.json()
|
|
315
|
+
else:
|
|
316
|
+
raise RecceException(f"Failed to download base session manifest for project {project_id}")
|
|
317
|
+
|
|
318
|
+
# Download catalog
|
|
319
|
+
response = requests.get(presigned_urls["catalog_url"])
|
|
320
|
+
if response.status_code == 200:
|
|
321
|
+
artifacts["catalog"] = response.json()
|
|
322
|
+
else:
|
|
323
|
+
raise RecceException(f"Failed to download base session catalog for project {project_id}")
|
|
324
|
+
|
|
325
|
+
return artifacts
|
|
326
|
+
|
|
327
|
+
def _export_state(self) -> Tuple[Union[str, None], str]:
|
|
328
|
+
"""
|
|
329
|
+
Export state to Recce Cloud based on catalog type.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
str: A message indicating the result of the export operation.
|
|
333
|
+
str: The etag of the exported state file (only used for GitHub).
|
|
334
|
+
"""
|
|
335
|
+
logger.info("Store recce state to Recce Cloud")
|
|
336
|
+
|
|
337
|
+
if self.catalog == "github":
|
|
338
|
+
return self._export_state_to_github()
|
|
339
|
+
elif self.catalog == "preview":
|
|
340
|
+
return self._export_state_to_preview()
|
|
341
|
+
elif self.catalog == "session":
|
|
342
|
+
return self._export_state_to_session()
|
|
343
|
+
else:
|
|
344
|
+
raise RecceException(f"Unsupported catalog type: {self.catalog}")
|
|
345
|
+
|
|
346
|
+
def _export_state_to_github(self) -> Tuple[Union[str, None], str]:
|
|
347
|
+
"""Export state to GitHub PR with metadata and etag."""
|
|
348
|
+
if (self.pr_info is None) or (self.pr_info.id is None) or (self.pr_info.repository is None):
|
|
349
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
350
|
+
|
|
351
|
+
# Generate metadata for GitHub only
|
|
352
|
+
check_status = CheckDAO().status()
|
|
353
|
+
metadata = {
|
|
354
|
+
"total_checks": check_status.get("total", 0),
|
|
355
|
+
"approved_checks": check_status.get("approved", 0),
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# Upload to Cloud
|
|
359
|
+
presigned_url = self.recce_cloud.get_presigned_url_by_github_repo(
|
|
360
|
+
method=PresignedUrlMethod.UPLOAD,
|
|
361
|
+
repository=self.pr_info.repository,
|
|
362
|
+
artifact_name=RECCE_STATE_COMPRESSED_FILE,
|
|
363
|
+
pr_id=self.pr_info.id,
|
|
364
|
+
metadata=metadata,
|
|
365
|
+
)
|
|
366
|
+
message = self._upload_state_to_url(
|
|
367
|
+
presigned_url=presigned_url,
|
|
368
|
+
file_type=SupportedFileTypes.GZIP,
|
|
369
|
+
password=self.cloud_options.get("password"),
|
|
370
|
+
metadata=metadata,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Get updated etag after upload
|
|
374
|
+
metadata_response = self._get_metadata_from_recce_cloud()
|
|
375
|
+
state_etag = metadata_response.get("etag") if metadata_response else None
|
|
376
|
+
|
|
377
|
+
if message:
|
|
378
|
+
logger.warning(message)
|
|
379
|
+
return message, state_etag
|
|
380
|
+
|
|
381
|
+
def _export_state_to_preview(self) -> Tuple[Union[str, None], None]:
|
|
382
|
+
"""Export state to preview share (no metadata or etag needed)."""
|
|
383
|
+
share_id = self.cloud_options.get("share_id")
|
|
384
|
+
presigned_url = self.recce_cloud.get_presigned_url_by_share_id(
|
|
385
|
+
method=PresignedUrlMethod.UPLOAD,
|
|
386
|
+
share_id=share_id,
|
|
387
|
+
metadata=None,
|
|
388
|
+
)
|
|
389
|
+
message = self._upload_state_to_url(
|
|
390
|
+
presigned_url=presigned_url, file_type=SupportedFileTypes.FILE, password=None, metadata=None
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if message:
|
|
394
|
+
logger.warning(message)
|
|
395
|
+
return message, None
|
|
396
|
+
|
|
397
|
+
def _export_state_to_session(self) -> Tuple[Union[str, None], None]:
|
|
398
|
+
"""Export state to session (upload recce_state with empty artifacts)."""
|
|
399
|
+
if self.session_id is None:
|
|
400
|
+
raise RecceException("Cannot export state to session. No session ID is provided.")
|
|
401
|
+
|
|
402
|
+
# Get session information
|
|
403
|
+
session = self.recce_cloud.get_session(self.session_id)
|
|
404
|
+
org_id = session.get("org_id")
|
|
405
|
+
project_id = session.get("project_id")
|
|
406
|
+
|
|
407
|
+
if not org_id or not project_id:
|
|
408
|
+
raise RecceException(f"Session {self.session_id} does not belong to a valid organization or project.")
|
|
409
|
+
|
|
410
|
+
# Get upload URLs (now includes recce_state_url)
|
|
411
|
+
presigned_urls = self.recce_cloud.get_upload_urls_by_session_id(org_id, project_id, self.session_id)
|
|
412
|
+
recce_state_url = presigned_urls.get("recce_state_url")
|
|
413
|
+
|
|
414
|
+
if not recce_state_url:
|
|
415
|
+
raise RecceException(f"No recce_state_url found for session {self.session_id}")
|
|
416
|
+
|
|
417
|
+
# Create a copy of the state with empty artifacts for upload
|
|
418
|
+
upload_state = RecceState()
|
|
419
|
+
upload_state.runs = self.state.runs.copy() if self.state.runs else []
|
|
420
|
+
upload_state.checks = []
|
|
421
|
+
# Keep artifacts empty (don't copy self.state.artifacts)
|
|
422
|
+
|
|
423
|
+
# Upload the state with empty artifacts
|
|
424
|
+
message = self._upload_state_to_url(
|
|
425
|
+
presigned_url=recce_state_url,
|
|
426
|
+
file_type=SupportedFileTypes.FILE,
|
|
427
|
+
password=None,
|
|
428
|
+
metadata=None,
|
|
429
|
+
state=upload_state,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
if message:
|
|
433
|
+
logger.warning(message)
|
|
434
|
+
else:
|
|
435
|
+
# Notify Recce Cloud that the state has been uploaded if upload is successful
|
|
436
|
+
self.recce_cloud.post_recce_state_uploaded_by_session_id(org_id, project_id, self.session_id)
|
|
437
|
+
return message, None
|
|
438
|
+
|
|
439
|
+
def _upload_state_to_url(
|
|
440
|
+
self,
|
|
441
|
+
presigned_url: str,
|
|
442
|
+
file_type: SupportedFileTypes,
|
|
443
|
+
password: str = None,
|
|
444
|
+
metadata: dict = None,
|
|
445
|
+
state: RecceState = None,
|
|
446
|
+
) -> Union[str, None]:
|
|
447
|
+
"""Upload state file to presigned URL."""
|
|
448
|
+
import tempfile
|
|
449
|
+
|
|
450
|
+
import requests
|
|
451
|
+
|
|
452
|
+
# Use provided state or default to self.state
|
|
453
|
+
upload_state = state or self.state
|
|
454
|
+
|
|
455
|
+
# Prepare headers
|
|
456
|
+
headers = {}
|
|
457
|
+
if password:
|
|
458
|
+
headers.update(s3_sse_c_headers(password))
|
|
459
|
+
if metadata:
|
|
460
|
+
headers["x-amz-tagging"] = urlencode(metadata)
|
|
461
|
+
|
|
462
|
+
with tempfile.NamedTemporaryFile() as tmp:
|
|
463
|
+
# Use the specified state to export to file
|
|
464
|
+
json_data = upload_state.to_json()
|
|
465
|
+
io = file_io_factory(file_type)
|
|
466
|
+
io.write(tmp.name, json_data)
|
|
467
|
+
|
|
468
|
+
with open(tmp.name, "rb") as fd:
|
|
469
|
+
response = requests.put(presigned_url, data=fd.read(), headers=headers)
|
|
470
|
+
|
|
471
|
+
if response.status_code not in [200, 204]:
|
|
472
|
+
self.error_message = response.text
|
|
473
|
+
return "Failed to upload the state file to Recce Cloud. Reason: " + response.text
|
|
474
|
+
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
def check_conflict(self) -> bool:
|
|
478
|
+
if self.catalog != "github":
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
metadata = self._get_metadata_from_recce_cloud()
|
|
482
|
+
if not metadata:
|
|
483
|
+
return False
|
|
484
|
+
|
|
485
|
+
state_etag = metadata.get("etag")
|
|
486
|
+
return state_etag != self.state_etag
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class RecceCloudStateManager:
|
|
490
|
+
error_message: str
|
|
491
|
+
hint_message: str
|
|
492
|
+
|
|
493
|
+
# It is a class to upload, download and purge the state file on Recce Cloud.
|
|
494
|
+
|
|
495
|
+
def __init__(self, cloud_options: Optional[Dict[str, str]] = None):
|
|
496
|
+
self.cloud_options = cloud_options or {}
|
|
497
|
+
self.pr_info = None
|
|
498
|
+
self.error_message = None
|
|
499
|
+
self.hint_message = None
|
|
500
|
+
self.github_token = self.cloud_options.get("github_token")
|
|
501
|
+
|
|
502
|
+
if not self.github_token:
|
|
503
|
+
raise RecceException(RECCE_CLOUD_TOKEN_MISSING.error_message)
|
|
504
|
+
self.pr_info = fetch_pr_metadata(cloud=True, github_token=self.github_token)
|
|
505
|
+
if self.pr_info.id is None:
|
|
506
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
507
|
+
|
|
508
|
+
def verify(self) -> bool:
|
|
509
|
+
if self.github_token is None:
|
|
510
|
+
self.error_message = RECCE_CLOUD_TOKEN_MISSING.error_message
|
|
511
|
+
self.hint_message = RECCE_CLOUD_TOKEN_MISSING.hint_message
|
|
512
|
+
return False
|
|
513
|
+
if self.cloud_options.get("password") is None:
|
|
514
|
+
self.error_message = RECCE_CLOUD_PASSWORD_MISSING.error_message
|
|
515
|
+
self.hint_message = RECCE_CLOUD_PASSWORD_MISSING.hint_message
|
|
516
|
+
return False
|
|
517
|
+
return True
|
|
518
|
+
|
|
519
|
+
@property
|
|
520
|
+
def error_and_hint(self) -> (Union[str, None], Union[str, None]):
|
|
521
|
+
return self.error_message, self.hint_message
|
|
522
|
+
|
|
523
|
+
def _check_state_in_recce_cloud(self) -> bool:
|
|
524
|
+
return RecceCloud(token=self.github_token).check_artifacts_exists(self.pr_info)
|
|
525
|
+
|
|
526
|
+
def check_cloud_state_exists(self) -> bool:
|
|
527
|
+
return self._check_state_in_recce_cloud()
|
|
528
|
+
|
|
529
|
+
def _upload_state_to_recce_cloud(self, state: RecceState, metadata: dict = None) -> Union[str, None]:
|
|
530
|
+
import tempfile
|
|
531
|
+
|
|
532
|
+
import requests
|
|
533
|
+
|
|
534
|
+
presigned_url = RecceCloud(token=self.github_token).get_presigned_url_by_github_repo(
|
|
535
|
+
method=PresignedUrlMethod.UPLOAD,
|
|
536
|
+
repository=self.pr_info.repository,
|
|
537
|
+
artifact_name=RECCE_STATE_COMPRESSED_FILE,
|
|
538
|
+
pr_id=self.pr_info.id,
|
|
539
|
+
metadata=metadata,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
compress_passwd = self.cloud_options.get("password")
|
|
543
|
+
headers = s3_sse_c_headers(compress_passwd)
|
|
544
|
+
with tempfile.NamedTemporaryFile() as tmp:
|
|
545
|
+
state.to_file(tmp.name, file_type=SupportedFileTypes.GZIP)
|
|
546
|
+
response = requests.put(presigned_url, data=open(tmp.name, "rb").read(), headers=headers)
|
|
547
|
+
if response.status_code != 200:
|
|
548
|
+
return f"Failed to upload the state file to Recce Cloud. Reason: {response.text}"
|
|
549
|
+
return "The state file is uploaded to Recce Cloud."
|
|
550
|
+
|
|
551
|
+
def upload_state_to_cloud(self, state: RecceState) -> Union[str, None]:
|
|
552
|
+
if (self.pr_info is None) or (self.pr_info.id is None) or (self.pr_info.repository is None):
|
|
553
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
554
|
+
|
|
555
|
+
checks = state.checks
|
|
556
|
+
|
|
557
|
+
metadata = {
|
|
558
|
+
"total_checks": len(checks),
|
|
559
|
+
"approved_checks": len([c for c in checks if c.is_checked]),
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return self._upload_state_to_recce_cloud(state, metadata)
|
|
563
|
+
|
|
564
|
+
def _download_state_from_recce_cloud(self, filepath):
|
|
565
|
+
import io
|
|
566
|
+
|
|
567
|
+
import requests
|
|
568
|
+
|
|
569
|
+
presigned_url = RecceCloud(token=self.github_token).get_presigned_url_by_github_repo(
|
|
570
|
+
method=PresignedUrlMethod.DOWNLOAD,
|
|
571
|
+
repository=self.pr_info.repository,
|
|
572
|
+
artifact_name=RECCE_STATE_COMPRESSED_FILE,
|
|
573
|
+
pr_id=self.pr_info.id,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
password = self.cloud_options.get("password")
|
|
577
|
+
if password is None:
|
|
578
|
+
raise RecceException(RECCE_CLOUD_PASSWORD_MISSING.error_message)
|
|
579
|
+
|
|
580
|
+
headers = s3_sse_c_headers(password)
|
|
581
|
+
response = requests.get(presigned_url, headers=headers)
|
|
582
|
+
|
|
583
|
+
if response.status_code != 200:
|
|
584
|
+
raise RecceException(
|
|
585
|
+
f"{response.status_code} Failed to download the state file from Recce Cloud. The password could be wrong."
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
byte_stream = io.BytesIO(response.content)
|
|
589
|
+
gzip_io = file_io_factory(SupportedFileTypes.GZIP)
|
|
590
|
+
decompressed_content = gzip_io.read_fileobj(byte_stream)
|
|
591
|
+
|
|
592
|
+
dirs = os.path.dirname(filepath)
|
|
593
|
+
if dirs:
|
|
594
|
+
os.makedirs(dirs, exist_ok=True)
|
|
595
|
+
with open(filepath, "wb") as f:
|
|
596
|
+
f.write(decompressed_content)
|
|
597
|
+
|
|
598
|
+
def download_state_from_cloud(self, filepath: str) -> Union[str, None]:
|
|
599
|
+
if (self.pr_info is None) or (self.pr_info.id is None) or (self.pr_info.repository is None):
|
|
600
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
601
|
+
|
|
602
|
+
logger.debug("Download state file from Recce Cloud...")
|
|
603
|
+
return self._download_state_from_recce_cloud(filepath)
|
|
604
|
+
|
|
605
|
+
def _purge_state_from_recce_cloud(self) -> (bool, str):
|
|
606
|
+
try:
|
|
607
|
+
RecceCloud(token=self.github_token).purge_artifacts(self.pr_info.repository, pr_id=self.pr_info.id)
|
|
608
|
+
except RecceCloudException as e:
|
|
609
|
+
return False, e.reason
|
|
610
|
+
return True, None
|
|
611
|
+
|
|
612
|
+
def purge_cloud_state(self) -> (bool, str):
|
|
613
|
+
return self._purge_state_from_recce_cloud()
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
class RecceShareStateManager:
|
|
617
|
+
error_message: str
|
|
618
|
+
hint_message: str
|
|
619
|
+
|
|
620
|
+
# It is a class to share state file on Recce Cloud.
|
|
621
|
+
|
|
622
|
+
def __init__(self, auth_options: Optional[Dict[str, str]] = None):
|
|
623
|
+
self.auth_options = auth_options or {}
|
|
624
|
+
self.error_message = None
|
|
625
|
+
self.hint_message = None
|
|
626
|
+
|
|
627
|
+
def verify(self) -> bool:
|
|
628
|
+
if get_recce_api_token() is None:
|
|
629
|
+
self.error_message = RECCE_API_TOKEN_MISSING.error_message
|
|
630
|
+
self.hint_message = RECCE_API_TOKEN_MISSING.hint_message
|
|
631
|
+
return False
|
|
632
|
+
return True
|
|
633
|
+
|
|
634
|
+
@property
|
|
635
|
+
def error_and_hint(self) -> (Union[str, None], Union[str, None]):
|
|
636
|
+
return self.error_message, self.hint_message
|
|
637
|
+
|
|
638
|
+
def share_state(self, file_name: str, state: RecceState) -> Dict:
|
|
639
|
+
import tempfile
|
|
640
|
+
|
|
641
|
+
with tempfile.NamedTemporaryFile() as tmp:
|
|
642
|
+
state.to_file(tmp.name, file_type=SupportedFileTypes.FILE)
|
|
643
|
+
response = RecceCloud(token=get_recce_api_token()).share_state(file_name, open(tmp.name, "rb"))
|
|
644
|
+
return response
|