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/const.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
RECCE_STATE_FILE = "recce-state.json"
|
|
4
|
+
RECCE_STATE_COMPRESSED_FILE = f"{RECCE_STATE_FILE}.gz"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ErrorMessage:
|
|
9
|
+
error_message: str
|
|
10
|
+
hint_message: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
RECCE_CLOUD_TOKEN_MISSING = ErrorMessage(
|
|
14
|
+
error_message="No GitHub token is provided to access the pull request information",
|
|
15
|
+
hint_message="Please provide a GitHub token in the command argument",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
RECCE_CLOUD_PASSWORD_MISSING = ErrorMessage(
|
|
19
|
+
error_message="No password provided to access the state file in Recce Cloud",
|
|
20
|
+
hint_message='Please provide a password with the option "--password <compress-password>"',
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
RECCE_API_TOKEN_MISSING = ErrorMessage(
|
|
24
|
+
error_message="No Recce API token is provided",
|
|
25
|
+
hint_message="Please login to Recce Cloud and copy the API token from the settings page",
|
|
26
|
+
)
|
recce/state/local.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
from .state import RecceState
|
|
6
|
+
from .state_loader import RecceStateLoader
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("uvicorn")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FileStateLoader(RecceStateLoader):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
review_mode: bool = False,
|
|
15
|
+
state_file: Optional[str] = None,
|
|
16
|
+
initial_state: Optional[RecceState] = None,
|
|
17
|
+
):
|
|
18
|
+
super().__init__(review_mode=review_mode, state_file=state_file, initial_state=initial_state)
|
|
19
|
+
|
|
20
|
+
def verify(self) -> bool:
|
|
21
|
+
if self.review_mode is True and self.state_file is None:
|
|
22
|
+
self.error_message = "Recce can not launch without a state file."
|
|
23
|
+
self.hint_message = "Please provide a state file in the command argument."
|
|
24
|
+
return False
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
def _load_state(self) -> Tuple[RecceState, str]:
|
|
28
|
+
state = RecceState.from_file(self.state_file) if self.state_file else None
|
|
29
|
+
state_tag = None
|
|
30
|
+
return state, state_tag
|
|
31
|
+
|
|
32
|
+
def _export_state(self, state: RecceState = None) -> Tuple[Union[str, None], str]:
|
|
33
|
+
"""
|
|
34
|
+
Store the state to a file. Store happens when terminating the server or run instance.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if self.state_file is None:
|
|
38
|
+
return "No state file is provided. Skip storing the state.", None
|
|
39
|
+
|
|
40
|
+
logger.info(f"Store recce state to '{self.state_file}'")
|
|
41
|
+
message = self._export_state_to_file(self.state_file)
|
|
42
|
+
tag = None
|
|
43
|
+
|
|
44
|
+
return message, tag
|
|
45
|
+
|
|
46
|
+
def purge(self) -> bool:
|
|
47
|
+
if self.state_file is not None:
|
|
48
|
+
try:
|
|
49
|
+
os.remove(self.state_file)
|
|
50
|
+
return True
|
|
51
|
+
except Exception as e:
|
|
52
|
+
self.error_message = f"Failed to remove the state file: {e}"
|
|
53
|
+
return False
|
|
54
|
+
else:
|
|
55
|
+
self.error_message = "No state file is provided. Skip removing the state file."
|
|
56
|
+
return False
|
recce/state/state.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Define the type to serialize/de-serialize the state of the recce instance."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from recce import get_version
|
|
11
|
+
from recce.exceptions import RecceException
|
|
12
|
+
from recce.git import current_branch
|
|
13
|
+
from recce.models.types import Check, Run
|
|
14
|
+
from recce.pull_request import PullRequestInfo
|
|
15
|
+
from recce.util.io import SupportedFileTypes, file_io_factory
|
|
16
|
+
from recce.util.pydantic_model import pydantic_model_dump, pydantic_model_json_dump
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("uvicorn")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GitRepoInfo(BaseModel):
|
|
22
|
+
branch: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def from_current_repository() -> Optional["GitRepoInfo"]:
|
|
26
|
+
branch = current_branch()
|
|
27
|
+
if branch is None:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
return GitRepoInfo(branch=branch)
|
|
31
|
+
|
|
32
|
+
def to_dict(self):
|
|
33
|
+
return pydantic_model_dump(self)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RecceStateMetadata(BaseModel):
|
|
37
|
+
schema_version: str = "v0"
|
|
38
|
+
recce_version: str = Field(default_factory=lambda: get_version())
|
|
39
|
+
generated_at: str = Field(default_factory=lambda: datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ArtifactsRoot(BaseModel):
|
|
43
|
+
"""
|
|
44
|
+
Root of the artifacts.
|
|
45
|
+
|
|
46
|
+
base: artifacts of the base env. key is file name, value is dict
|
|
47
|
+
current: artifacts of the current env. key is file name, value is dict
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
base: Dict[str, Optional[dict]] = {}
|
|
51
|
+
current: Dict[str, Optional[dict]] = {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RecceState(BaseModel):
|
|
55
|
+
metadata: Optional[RecceStateMetadata] = None
|
|
56
|
+
runs: Optional[List[Run]] = Field(default_factory=list)
|
|
57
|
+
checks: Optional[List[Check]] = Field(default_factory=list)
|
|
58
|
+
artifacts: ArtifactsRoot = ArtifactsRoot(base={}, current={})
|
|
59
|
+
git: Optional[GitRepoInfo] = None
|
|
60
|
+
pull_request: Optional[PullRequestInfo] = None
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def from_json(json_content: str):
|
|
64
|
+
dict_data = json.loads(json_content)
|
|
65
|
+
state = RecceState(**dict_data)
|
|
66
|
+
metadata = state.metadata
|
|
67
|
+
|
|
68
|
+
if metadata:
|
|
69
|
+
if metadata.schema_version is None:
|
|
70
|
+
pass
|
|
71
|
+
if metadata.schema_version == "v0":
|
|
72
|
+
pass
|
|
73
|
+
else:
|
|
74
|
+
raise RecceException(f"Unsupported state file version: {metadata.schema_version}")
|
|
75
|
+
return state
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def from_file(file_path: str, file_type: SupportedFileTypes = SupportedFileTypes.FILE):
|
|
79
|
+
"""
|
|
80
|
+
Load the state from a recce state file.
|
|
81
|
+
"""
|
|
82
|
+
from pathlib import Path
|
|
83
|
+
|
|
84
|
+
logger.debug(f"Load state file from: '{file_path}'")
|
|
85
|
+
if not Path(file_path).is_file():
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
io = file_io_factory(file_type)
|
|
89
|
+
json_content = io.read(file_path)
|
|
90
|
+
return RecceState.from_json(json_content)
|
|
91
|
+
|
|
92
|
+
def to_json(self):
|
|
93
|
+
return pydantic_model_json_dump(self)
|
|
94
|
+
|
|
95
|
+
def to_file(self, file_path: str, file_type: SupportedFileTypes = SupportedFileTypes.FILE):
|
|
96
|
+
|
|
97
|
+
json_data = self.to_json()
|
|
98
|
+
io = file_io_factory(file_type)
|
|
99
|
+
|
|
100
|
+
io.write(file_path, json_data)
|
|
101
|
+
return f"The state file is stored at '{file_path}'"
|
|
102
|
+
|
|
103
|
+
def _merge_run(self, run: Run):
|
|
104
|
+
for r in self.runs:
|
|
105
|
+
if r.run_id == run.run_id:
|
|
106
|
+
break
|
|
107
|
+
else:
|
|
108
|
+
self.runs.append(run)
|
|
109
|
+
|
|
110
|
+
def _merge_check(self, check: Check):
|
|
111
|
+
for c in self.checks:
|
|
112
|
+
if c.check_id == check.check_id:
|
|
113
|
+
c.merge(check)
|
|
114
|
+
break
|
|
115
|
+
else:
|
|
116
|
+
self.checks.append(check)
|
|
117
|
+
|
|
118
|
+
def _merge_artifacts(self, artifacts: ArtifactsRoot):
|
|
119
|
+
self.artifacts.merge(artifacts)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Dict, Literal, Optional, Tuple, Union, final
|
|
6
|
+
|
|
7
|
+
from recce.exceptions import RecceException
|
|
8
|
+
from recce.pull_request import fetch_pr_metadata
|
|
9
|
+
|
|
10
|
+
from ..util.io import SupportedFileTypes, file_io_factory
|
|
11
|
+
from .const import RECCE_API_TOKEN_MISSING
|
|
12
|
+
from .state import RecceState
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("uvicorn")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RecceStateLoader(ABC):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
review_mode: bool = False,
|
|
21
|
+
cloud_mode: bool = False,
|
|
22
|
+
state_file: Optional[str] = None,
|
|
23
|
+
cloud_options: Optional[Dict[str, str]] = None,
|
|
24
|
+
initial_state: Optional[RecceState] = None,
|
|
25
|
+
):
|
|
26
|
+
self.review_mode = review_mode
|
|
27
|
+
self.cloud_mode = cloud_mode
|
|
28
|
+
self.state_file = state_file
|
|
29
|
+
self.cloud_options = cloud_options or {}
|
|
30
|
+
self.error_message = None
|
|
31
|
+
self.hint_message = None
|
|
32
|
+
self.state: RecceState | None = initial_state
|
|
33
|
+
self.state_lock = threading.Lock()
|
|
34
|
+
self.state_etag = None
|
|
35
|
+
self.pr_info = None
|
|
36
|
+
self.catalog: Literal["github", "preview", "session"] = "github"
|
|
37
|
+
self.share_id = None
|
|
38
|
+
self.session_id = None
|
|
39
|
+
|
|
40
|
+
if self.cloud_mode:
|
|
41
|
+
if self.cloud_options.get("github_token"):
|
|
42
|
+
self.catalog = "github"
|
|
43
|
+
self.pr_info = fetch_pr_metadata(
|
|
44
|
+
cloud=self.cloud_mode, github_token=self.cloud_options.get("github_token")
|
|
45
|
+
)
|
|
46
|
+
if self.pr_info.id is None:
|
|
47
|
+
raise RecceException("Cannot get the pull request information from GitHub.")
|
|
48
|
+
elif self.cloud_options.get("api_token"):
|
|
49
|
+
if self.cloud_options.get("session_id"):
|
|
50
|
+
self.catalog = "session"
|
|
51
|
+
self.session_id = self.cloud_options.get("session_id")
|
|
52
|
+
else:
|
|
53
|
+
self.catalog = "preview"
|
|
54
|
+
self.share_id = self.cloud_options.get("share_id")
|
|
55
|
+
else:
|
|
56
|
+
raise RecceException(RECCE_API_TOKEN_MISSING.error_message)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def token(self):
|
|
60
|
+
return self.cloud_options.get("github_token") or self.cloud_options.get("api_token")
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def verify(self) -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Verify the state loader configuration.
|
|
66
|
+
Returns:
|
|
67
|
+
bool: True if the configuration is valid, False otherwise.
|
|
68
|
+
"""
|
|
69
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def error_and_hint(self) -> (Union[str, None], Union[str, None]):
|
|
73
|
+
return self.error_message, self.hint_message
|
|
74
|
+
|
|
75
|
+
def update(self, state: RecceState):
|
|
76
|
+
self.state = state
|
|
77
|
+
|
|
78
|
+
@final
|
|
79
|
+
def load(self, refresh=False) -> RecceState:
|
|
80
|
+
if self.state is not None and refresh is False:
|
|
81
|
+
return self.state
|
|
82
|
+
self.state_lock.acquire()
|
|
83
|
+
try:
|
|
84
|
+
self.state, self.state_etag = self._load_state()
|
|
85
|
+
finally:
|
|
86
|
+
self.state_lock.release()
|
|
87
|
+
return self.state
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def _load_state(self) -> Tuple[RecceState, str]:
|
|
91
|
+
"""
|
|
92
|
+
Load the state from the specified source (file or cloud).
|
|
93
|
+
Returns:
|
|
94
|
+
RecceState: The loaded state object.
|
|
95
|
+
str: The etag of the state file (if applicable).
|
|
96
|
+
"""
|
|
97
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
98
|
+
|
|
99
|
+
def save_as(self, state_file: str, state: RecceState = None):
|
|
100
|
+
if self.cloud_mode:
|
|
101
|
+
raise Exception("Cannot save the state to Recce Cloud.")
|
|
102
|
+
|
|
103
|
+
self.state_file = state_file
|
|
104
|
+
self.export(state)
|
|
105
|
+
|
|
106
|
+
@final
|
|
107
|
+
def export(self, state: RecceState = None) -> Union[str, None]:
|
|
108
|
+
if state is not None:
|
|
109
|
+
self.update(state)
|
|
110
|
+
|
|
111
|
+
start_time = time.time()
|
|
112
|
+
self.state_lock.acquire()
|
|
113
|
+
try:
|
|
114
|
+
message, state_etag = self._export_state()
|
|
115
|
+
self.state_etag = state_etag
|
|
116
|
+
end_time = time.time()
|
|
117
|
+
elapsed_time = end_time - start_time
|
|
118
|
+
finally:
|
|
119
|
+
self.state_lock.release()
|
|
120
|
+
logger.info(f"Store state completed in {elapsed_time:.2f} seconds")
|
|
121
|
+
return message
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def _export_state(self) -> Tuple[Union[str, None], str]:
|
|
125
|
+
"""
|
|
126
|
+
Export the current Recce state to a file or cloud storage.
|
|
127
|
+
Returns:
|
|
128
|
+
str: A message indicating the result of the export operation.
|
|
129
|
+
str: The etag of the exported state file (if applicable).
|
|
130
|
+
"""
|
|
131
|
+
raise NotImplementedError("Subclasses must implement this method.")
|
|
132
|
+
|
|
133
|
+
def _export_state_to_file(self, file_path: str, file_type: SupportedFileTypes = SupportedFileTypes.FILE) -> str:
|
|
134
|
+
"""
|
|
135
|
+
Store the state to a file. Store happens when terminating the server or run instance.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
json_data = self.state.to_json()
|
|
139
|
+
io = file_io_factory(file_type)
|
|
140
|
+
|
|
141
|
+
io.write(file_path, json_data)
|
|
142
|
+
return f"The state file is stored at '{file_path}'"
|
|
143
|
+
|
|
144
|
+
def refresh(self):
|
|
145
|
+
new_state = self.load(refresh=True)
|
|
146
|
+
return new_state
|
|
147
|
+
|
|
148
|
+
def check_conflict(self) -> bool:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
def info(self) -> dict:
|
|
152
|
+
if self.state is None:
|
|
153
|
+
self.error_message = "No state is loaded."
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
state_info = {
|
|
157
|
+
"mode": "cloud" if self.cloud_mode else "local",
|
|
158
|
+
"source": None,
|
|
159
|
+
}
|
|
160
|
+
if self.cloud_mode:
|
|
161
|
+
state_info["source"] = "Recce Cloud"
|
|
162
|
+
state_info["pull_request"] = self.pr_info
|
|
163
|
+
else:
|
|
164
|
+
state_info["source"] = self.state_file
|
|
165
|
+
return state_info
|
|
166
|
+
|
|
167
|
+
@abstractmethod
|
|
168
|
+
def purge(self) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
Purge the state file or cloud storage.
|
|
171
|
+
Returns:
|
|
172
|
+
bool: True if the purge was successful, False otherwise.
|
|
173
|
+
"""
|
|
174
|
+
raise NotImplementedError("Subclasses must implement this method.")
|