recce-nightly 1.10.0.20250625__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 +5 -0
- recce/adapter/dbt_adapter/__init__.py +343 -245
- recce/apis/check_api.py +20 -14
- recce/apis/check_events_api.py +353 -0
- recce/apis/check_func.py +5 -5
- recce/apis/run_func.py +32 -3
- recce/artifact.py +76 -3
- recce/cli.py +705 -82
- recce/config.py +2 -2
- recce/connect_to_cloud.py +1 -1
- recce/core.py +3 -3
- recce/data/404/index.html +2 -0
- recce/data/404.html +2 -22
- 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.bd5c9f50.woff → montserrat-cyrillic-800-normal.f9d58125.woff} +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → montserrat-latin-800-normal.d5761935.woff} +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-latin-ext-800-normal.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
- recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_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 +1 -1
- 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/index.html +2 -27
- recce/data/index.txt +32 -8
- 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/event/CONFIG.bak +1 -0
- recce/event/__init__.py +9 -8
- recce/event/collector.py +6 -2
- recce/event/track.py +10 -0
- recce/github.py +1 -1
- recce/mcp_server.py +725 -0
- recce/models/check.py +433 -15
- recce/models/types.py +61 -2
- recce/pull_request.py +1 -1
- recce/run.py +37 -17
- recce/server.py +216 -21
- 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 +25 -3
- recce/tasks/dataframe.py +63 -1
- recce/tasks/query.py +40 -3
- recce/tasks/rowcount.py +4 -1
- recce/tasks/schema.py +4 -1
- recce/tasks/utils.py +147 -0
- recce/tasks/valuediff.py +85 -57
- recce/util/api_token.py +11 -2
- recce/util/breaking.py +10 -1
- recce/util/cll.py +1 -2
- 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 +2 -2
- recce/util/lineage.py +19 -18
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +254 -5
- recce/util/startup_perf.py +121 -0
- recce/yaml/__init__.py +2 -2
- {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/METADATA +91 -71
- recce_nightly-1.30.0.20251221.dist-info/RECORD +183 -0
- {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/WHEEL +1 -2
- recce/data/_next/static/abCX3x3UoIdRLEDWxx4xd/_buildManifest.js +0 -1
- recce/data/_next/static/chunks/181-acc61ddada3bc0ca.js +0 -43
- recce/data/_next/static/chunks/1bff33f1-1ef85cf5e658a751.js +0 -1
- recce/data/_next/static/chunks/217-879a84d70f7a907c.js +0 -2
- recce/data/_next/static/chunks/29e3cc0d-60045b2e47aa3916.js +0 -1
- recce/data/_next/static/chunks/36e1c10d-8e7be4a6c1f6ab2d.js +0 -1
- recce/data/_next/static/chunks/3998a672-03adacad07b346ac.js +0 -1
- recce/data/_next/static/chunks/3a92ee20-1081c360214f9602.js +0 -1
- recce/data/_next/static/chunks/42-cd3c06533f5fd47c.js +0 -9
- recce/data/_next/static/chunks/450c323b-fd94e7ffaa4a5efa.js +0 -1
- recce/data/_next/static/chunks/47d8844f-929aed9b1c73a905.js +0 -1
- recce/data/_next/static/chunks/608-3b079b544e5d5f5e.js +0 -15
- recce/data/_next/static/chunks/6dc81886-adbfa45836061d79.js +0 -1
- recce/data/_next/static/chunks/7a8a3e83-edf6dc64b5d5f0a5.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-d5f0438edd5c2a5b.js +0 -1
- recce/data/_next/static/chunks/86730205-cfb14e3f051bab35.js +0 -1
- recce/data/_next/static/chunks/8d700b6a.8bb140898499c512.js +0 -1
- recce/data/_next/static/chunks/92-607cd1af83c41f43.js +0 -1
- recce/data/_next/static/chunks/9746af58-a42b7d169cacadf0.js +0 -1
- recce/data/_next/static/chunks/a30376cd-de84559016d7e133.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-01ed58b7f971d311.js +0 -1
- recce/data/_next/static/chunks/app/layout-177a410a97e0d018.js +0 -1
- recce/data/_next/static/chunks/app/page-da6e046a8235dbfc.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-4282bdcf459e075c.js +0 -1
- recce/data/_next/static/chunks/bbda5537-9ec25eb1dd62348a.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-08cb668a789d6afd.js +0 -1
- recce/data/_next/static/chunks/ce84277d-2e5d1d46910cf052.js +0 -1
- recce/data/_next/static/chunks/febdd86e-c6b525341634b860.js +0 -54
- recce/data/_next/static/chunks/fee69bc6-2dbccaf9b90474e6.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-app-39061b0166c47f55.js +0 -1
- recce/data/_next/static/chunks/main-b5b3ae20a1405261.js +0 -1
- recce/data/_next/static/chunks/pages/_app-437c455677d62394.js +0 -1
- recce/data/_next/static/chunks/pages/_error-e7650df18ca04bde.js +0 -1
- recce/data/_next/static/chunks/webpack-7b49d5ba7e3a434d.js +0 -1
- recce/data/_next/static/css/17a96168e3a9db13.css +0 -1
- recce/data/_next/static/css/1b121dc4d36aeb4d.css +0 -3
- recce/data/_next/static/css/35c6679a098e1e34.css +0 -1
- recce/data/_next/static/css/951e2e0eea2d4a5b.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/data/_next/static/media/reload-image.79aabb7d.svg +0 -4
- recce/state.py +0 -786
- recce_nightly-1.10.0.20250625.dist-info/RECORD +0 -154
- recce_nightly-1.10.0.20250625.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 -17
- tests/adapter/dbt_adapter/dbt_test_helper.py +0 -298
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -25
- tests/adapter/dbt_adapter/test_dbt_cll.py +0 -384
- tests/adapter/dbt_adapter/test_selector.py +0 -202
- tests/tasks/__init__.py +0 -0
- tests/tasks/conftest.py +0 -4
- tests/tasks/test_histogram.py +0 -129
- tests/tasks/test_lineage.py +0 -55
- tests/tasks/test_preset_checks.py +0 -64
- tests/tasks/test_profile.py +0 -397
- tests/tasks/test_query.py +0 -151
- tests/tasks/test_row_count.py +0 -135
- tests/tasks/test_schema.py +0 -122
- tests/tasks/test_top_k.py +0 -77
- tests/tasks/test_valuediff.py +0 -85
- tests/test_cli.py +0 -133
- tests/test_config.py +0 -43
- tests/test_connect_to_cloud.py +0 -82
- tests/test_core.py +0 -29
- tests/test_dbt.py +0 -36
- tests/test_pull_request.py +0 -130
- tests/test_server.py +0 -104
- tests/test_state.py +0 -134
- tests/test_summary.py +0 -65
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
- /recce/data/_next/static/{abCX3x3UoIdRLEDWxx4xd → nX-Uz0AH6Tc6hIQUFGqaB}/_ssgManifest.js +0 -0
- {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/licenses/LICENSE +0 -0
recce/models/check.py
CHANGED
|
@@ -1,43 +1,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CheckDAO with cloud integration.
|
|
3
|
+
|
|
4
|
+
This module provides data access for Check objects with support for both
|
|
5
|
+
local (in-memory) and cloud (Recce Cloud API) storage modes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import typing
|
|
10
|
+
from datetime import datetime, timezone
|
|
1
11
|
from typing import List, Optional
|
|
12
|
+
from uuid import UUID
|
|
2
13
|
|
|
3
14
|
from recce.exceptions import RecceException
|
|
4
15
|
|
|
5
|
-
from .types import Check
|
|
16
|
+
from .types import Check, RunType
|
|
17
|
+
|
|
18
|
+
if typing.TYPE_CHECKING:
|
|
19
|
+
from ..apis.check_api import PatchCheckIn
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("uvicorn")
|
|
6
22
|
|
|
7
23
|
|
|
8
24
|
class CheckDAO:
|
|
9
25
|
"""
|
|
10
|
-
Data Access Object for Check.
|
|
26
|
+
Data Access Object for Check.
|
|
27
|
+
|
|
28
|
+
Supports two modes:
|
|
29
|
+
- Local mode: Stores checks in memory
|
|
30
|
+
- Cloud mode: Stores checks in Recce Cloud via API
|
|
31
|
+
|
|
32
|
+
The mode is determined by checking if a session_id exists in the state_loader.
|
|
11
33
|
"""
|
|
12
34
|
|
|
35
|
+
def __init__(self):
|
|
36
|
+
"""Initialize CheckDAO."""
|
|
37
|
+
self._session_info_cache = None
|
|
38
|
+
|
|
13
39
|
@property
|
|
14
40
|
def _checks(self):
|
|
41
|
+
"""Get checks from local context."""
|
|
15
42
|
from recce.core import default_context
|
|
16
43
|
|
|
17
44
|
return default_context().checks
|
|
18
45
|
|
|
19
|
-
|
|
20
|
-
|
|
46
|
+
@property
|
|
47
|
+
def is_cloud_user(self) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Determine if the user is in cloud mode.
|
|
50
|
+
|
|
51
|
+
Returns True if state_loader has a session_id, indicating cloud mode.
|
|
52
|
+
Returns False otherwise, indicating local mode.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
bool: True if cloud mode, False if local mode
|
|
56
|
+
"""
|
|
57
|
+
from recce.core import default_context
|
|
58
|
+
|
|
59
|
+
ctx = default_context()
|
|
60
|
+
if ctx is None or ctx.state_loader is None:
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
return hasattr(ctx.state_loader, "session_id") and ctx.state_loader.session_id is not None
|
|
64
|
+
|
|
65
|
+
def _get_session_info(self) -> tuple[str, str, str]:
|
|
66
|
+
"""
|
|
67
|
+
Get organization ID, project ID, and session ID from state loader.
|
|
68
|
+
|
|
69
|
+
Caches the session info to avoid repeated API calls.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
tuple: (org_id, project_id, session_id)
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
RecceException: If session info cannot be retrieved
|
|
76
|
+
"""
|
|
77
|
+
from recce.core import default_context
|
|
78
|
+
|
|
79
|
+
if self._session_info_cache is not None:
|
|
80
|
+
return self._session_info_cache
|
|
81
|
+
|
|
82
|
+
ctx = default_context()
|
|
83
|
+
state_loader = ctx.state_loader
|
|
84
|
+
|
|
85
|
+
if not hasattr(state_loader, "session_id") or state_loader.session_id is None:
|
|
86
|
+
raise RecceException("Cannot get session info: no session_id in state_loader")
|
|
87
|
+
|
|
88
|
+
session_id = state_loader.session_id
|
|
89
|
+
|
|
90
|
+
# Get org_id and project_id from the session
|
|
91
|
+
# First check if they're already cached on the state_loader
|
|
92
|
+
if hasattr(state_loader, "org_id") and hasattr(state_loader, "project_id"):
|
|
93
|
+
org_id = state_loader.org_id
|
|
94
|
+
project_id = state_loader.project_id
|
|
95
|
+
else:
|
|
96
|
+
# Fetch from cloud API
|
|
97
|
+
from recce.event import get_recce_api_token
|
|
98
|
+
from recce.util.recce_cloud import RecceCloud
|
|
99
|
+
|
|
100
|
+
api_token = get_recce_api_token() or ctx.state_loader.token
|
|
101
|
+
if not api_token:
|
|
102
|
+
raise RecceException("Cannot access Recce Cloud: no API token available")
|
|
103
|
+
|
|
104
|
+
recce_cloud = RecceCloud(api_token)
|
|
105
|
+
session = recce_cloud.get_session(session_id)
|
|
106
|
+
|
|
107
|
+
org_id = session.get("org_id")
|
|
108
|
+
project_id = session.get("project_id")
|
|
109
|
+
|
|
110
|
+
if not org_id or not project_id:
|
|
111
|
+
raise RecceException(f"Session {session_id} does not belong to a valid organization or project")
|
|
112
|
+
|
|
113
|
+
# Cache on state_loader for future use
|
|
114
|
+
state_loader.org_id = org_id
|
|
115
|
+
state_loader.project_id = project_id
|
|
116
|
+
|
|
117
|
+
self._session_info_cache = (org_id, project_id, session_id)
|
|
118
|
+
return self._session_info_cache
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _get_cloud_client():
|
|
122
|
+
"""
|
|
123
|
+
Get the cloud checks client.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
ChecksCloud: Cloud client for check operations
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
RecceException: If cloud client cannot be initialized
|
|
130
|
+
"""
|
|
131
|
+
from recce.core import default_context
|
|
132
|
+
from recce.event import get_recce_api_token
|
|
133
|
+
from recce.util.recce_cloud import RecceCloud
|
|
134
|
+
|
|
135
|
+
ctx = default_context()
|
|
136
|
+
|
|
137
|
+
api_token = get_recce_api_token() or ctx.state_loader.token
|
|
138
|
+
if not api_token:
|
|
139
|
+
raise RecceException("Cannot access Recce Cloud: no API token available")
|
|
140
|
+
|
|
141
|
+
recce_cloud = RecceCloud(api_token)
|
|
142
|
+
return recce_cloud.checks
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _check_to_cloud_format(check: Check) -> dict:
|
|
146
|
+
"""
|
|
147
|
+
Convert a Check object to cloud API format.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
check: Check object to convert
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
dict: Check data in cloud API format
|
|
154
|
+
"""
|
|
155
|
+
return {
|
|
156
|
+
"session_id": str(check.session_id) if check.session_id else None,
|
|
157
|
+
"name": check.name,
|
|
158
|
+
"type": check.type.value,
|
|
159
|
+
"params": check.params or {},
|
|
160
|
+
"created_by": check.created_by,
|
|
161
|
+
"description": check.description,
|
|
162
|
+
"view_options": check.view_options or {},
|
|
163
|
+
"is_checked": check.is_checked,
|
|
164
|
+
"is_preset": check.is_preset,
|
|
165
|
+
"updated_by": check.updated_by,
|
|
166
|
+
"created_at": check.created_at.isoformat() if check.created_at else None,
|
|
167
|
+
"updated_at": check.updated_at.isoformat() if check.updated_at else None,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _cloud_to_check(cloud_data: dict) -> Check:
|
|
172
|
+
"""
|
|
173
|
+
Convert cloud API data to a Check object.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
cloud_data: Check data from cloud API
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Check: Check object
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
logger.debug(f"Converting cloud data to Check object for check: {cloud_data.get('id')}")
|
|
183
|
+
# Parse the type
|
|
184
|
+
check_type = RunType(cloud_data.get("type"))
|
|
185
|
+
|
|
186
|
+
return Check(
|
|
187
|
+
check_id=UUID(cloud_data.get("id")),
|
|
188
|
+
session_id=UUID(cloud_data.get("session_id")),
|
|
189
|
+
name=cloud_data.get("name"),
|
|
190
|
+
description=cloud_data.get("description", ""),
|
|
191
|
+
type=check_type,
|
|
192
|
+
params=cloud_data.get("params", {}),
|
|
193
|
+
view_options=cloud_data.get("view_options", {}),
|
|
194
|
+
is_checked=cloud_data.get("is_checked", False),
|
|
195
|
+
is_preset=cloud_data.get("is_preset", False),
|
|
196
|
+
created_by=(cloud_data.get("created_by") or {}).get("email", ""),
|
|
197
|
+
updated_by=(cloud_data.get("updated_by") or {}).get("email", ""),
|
|
198
|
+
created_at=datetime.fromisoformat(cloud_data["created_at"]) if cloud_data.get("created_at") else None,
|
|
199
|
+
updated_at=datetime.fromisoformat(cloud_data["updated_at"]) if cloud_data.get("updated_at") else None,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def create(self, check: Check) -> Check:
|
|
203
|
+
"""
|
|
204
|
+
Create a new check.
|
|
205
|
+
|
|
206
|
+
In local mode: Appends check to in-memory list
|
|
207
|
+
In cloud mode: Creates check via Recce Cloud API
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
check: Check object to create
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
RecceException: If creation fails in cloud mode
|
|
214
|
+
"""
|
|
215
|
+
if self.is_cloud_user:
|
|
216
|
+
try:
|
|
217
|
+
org_id, project_id, session_id = self._get_session_info()
|
|
218
|
+
cloud_client = self._get_cloud_client()
|
|
219
|
+
|
|
220
|
+
check_data = self._check_to_cloud_format(check)
|
|
221
|
+
cloud_check = cloud_client.create_check(org_id, project_id, session_id, check_data)
|
|
222
|
+
new_check = self._cloud_to_check(cloud_check)
|
|
223
|
+
|
|
224
|
+
logger.debug(f"Created check {new_check.check_id} in cloud")
|
|
225
|
+
return new_check
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Failed to create check in cloud: {e}")
|
|
228
|
+
raise RecceException(f"Failed to create check in Recce Cloud: {e}")
|
|
229
|
+
else:
|
|
230
|
+
# Local mode
|
|
231
|
+
self._checks.append(check)
|
|
232
|
+
return check
|
|
21
233
|
|
|
22
234
|
def find_check_by_id(self, check_id) -> Optional[Check]:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
235
|
+
"""
|
|
236
|
+
Find a check by its ID.
|
|
237
|
+
|
|
238
|
+
In local mode: Searches in-memory list
|
|
239
|
+
In cloud mode: Retrieves check from Recce Cloud API
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
check_id: Check ID (UUID or string)
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Check object if found, None otherwise
|
|
246
|
+
"""
|
|
247
|
+
if self.is_cloud_user:
|
|
248
|
+
try:
|
|
249
|
+
org_id, project_id, session_id = self._get_session_info()
|
|
250
|
+
cloud_client = self._get_cloud_client()
|
|
251
|
+
|
|
252
|
+
cloud_data = cloud_client.get_check(org_id, project_id, session_id, str(check_id))
|
|
253
|
+
return self._cloud_to_check(cloud_data)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(f"Failed to get check {check_id} from cloud: {e}")
|
|
256
|
+
return None
|
|
257
|
+
else:
|
|
258
|
+
# Local mode
|
|
259
|
+
for check in self._checks:
|
|
260
|
+
if str(check_id) == str(check.check_id):
|
|
261
|
+
return check
|
|
262
|
+
return None
|
|
26
263
|
|
|
27
|
-
|
|
264
|
+
def update_check_by_id(self, check_id, patch: "PatchCheckIn") -> Optional[Check]:
|
|
265
|
+
"""
|
|
266
|
+
Update a check by its ID.
|
|
267
|
+
|
|
268
|
+
In local mode: Updates in-memory list
|
|
269
|
+
In cloud mode: Updates via Recce Cloud API
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
check_id: Check ID (UUID or string)
|
|
273
|
+
patch: Partial Check object with updated data
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
bool: True if updated, False if not found
|
|
277
|
+
"""
|
|
278
|
+
if self.is_cloud_user:
|
|
279
|
+
try:
|
|
280
|
+
org_id, project_id, session_id = self._get_session_info()
|
|
281
|
+
cloud_client = self._get_cloud_client()
|
|
282
|
+
|
|
283
|
+
# Directly send the patch object to the cloud API
|
|
284
|
+
cloud_data = cloud_client.update_check(
|
|
285
|
+
org_id, project_id, session_id, str(check_id), patch.model_dump(exclude_unset=True)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
logger.debug(f"Updated check {check_id} in cloud")
|
|
289
|
+
return self._cloud_to_check(cloud_data)
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.error(f"Failed to update check {check_id} in cloud: {e}")
|
|
292
|
+
return None
|
|
293
|
+
else:
|
|
294
|
+
# Local mode
|
|
295
|
+
check = CheckDAO().find_check_by_id(check_id)
|
|
296
|
+
if check is None:
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
if patch.name is not None:
|
|
300
|
+
check.name = patch.name
|
|
301
|
+
if patch.description is not None:
|
|
302
|
+
check.description = patch.description
|
|
303
|
+
if patch.params is not None:
|
|
304
|
+
check.params = patch.params
|
|
305
|
+
if patch.view_options is not None:
|
|
306
|
+
check.view_options = patch.view_options
|
|
307
|
+
if patch.is_checked is not None:
|
|
308
|
+
check.is_checked = patch.is_checked
|
|
309
|
+
check.updated_at = datetime.now(timezone.utc).replace(microsecond=0)
|
|
310
|
+
|
|
311
|
+
return check
|
|
28
312
|
|
|
29
313
|
def delete(self, check_id) -> bool:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
314
|
+
"""
|
|
315
|
+
Delete a check by its ID.
|
|
316
|
+
|
|
317
|
+
In local mode: Removes from in-memory list
|
|
318
|
+
In cloud mode: Deletes via Recce Cloud API
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
check_id: Check ID (UUID or string)
|
|
34
322
|
|
|
35
|
-
|
|
323
|
+
Returns:
|
|
324
|
+
bool: True if deleted, False if not found
|
|
325
|
+
"""
|
|
326
|
+
if self.is_cloud_user:
|
|
327
|
+
try:
|
|
328
|
+
org_id, project_id, session_id = self._get_session_info()
|
|
329
|
+
cloud_client = self._get_cloud_client()
|
|
330
|
+
|
|
331
|
+
cloud_client.delete_check(org_id, project_id, session_id, str(check_id))
|
|
332
|
+
logger.debug(f"Deleted check {check_id} from cloud")
|
|
333
|
+
return True
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.error(f"Failed to delete check {check_id} from cloud: {e}")
|
|
336
|
+
return False
|
|
337
|
+
else:
|
|
338
|
+
# Local mode
|
|
339
|
+
for check in self._checks:
|
|
340
|
+
if str(check_id) == str(check.check_id):
|
|
341
|
+
self._checks.remove(check)
|
|
342
|
+
return True
|
|
343
|
+
return False
|
|
36
344
|
|
|
37
345
|
def list(self) -> List[Check]:
|
|
38
|
-
|
|
346
|
+
"""
|
|
347
|
+
List all checks.
|
|
348
|
+
|
|
349
|
+
In local mode: Returns copy of in-memory list
|
|
350
|
+
In cloud mode: Retrieves all checks from Recce Cloud API
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
List of Check objects
|
|
354
|
+
"""
|
|
355
|
+
if self.is_cloud_user:
|
|
356
|
+
try:
|
|
357
|
+
org_id, project_id, session_id = self._get_session_info()
|
|
358
|
+
logger.debug(f"Listing checks from cloud: {org_id}:{project_id}:{session_id}")
|
|
359
|
+
cloud_client = self._get_cloud_client()
|
|
360
|
+
|
|
361
|
+
cloud_checks = cloud_client.list_checks(org_id, project_id, session_id)
|
|
362
|
+
return [self._cloud_to_check(check_data) for check_data in cloud_checks]
|
|
363
|
+
except AttributeError as e:
|
|
364
|
+
logger.error(f"Attribute error while listing checks from cloud: {e}")
|
|
365
|
+
return []
|
|
366
|
+
except Exception as e:
|
|
367
|
+
logger.exception(e)
|
|
368
|
+
# Return empty list on error to avoid breaking the UI
|
|
369
|
+
return []
|
|
370
|
+
else:
|
|
371
|
+
# Local mode
|
|
372
|
+
return list(self._checks)
|
|
39
373
|
|
|
40
374
|
def reorder(self, source: int, destination: int):
|
|
375
|
+
"""
|
|
376
|
+
Reorder checks.
|
|
377
|
+
|
|
378
|
+
Note: This operation is only supported in local mode.
|
|
379
|
+
In cloud mode, raises an exception as reordering must be handled server-side.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
source: Source index
|
|
383
|
+
destination: Destination index
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
RecceException: If indices are out of range or if in cloud mode
|
|
387
|
+
"""
|
|
388
|
+
if self.is_cloud_user:
|
|
389
|
+
raise RecceException(
|
|
390
|
+
"Reordering checks is not supported in cloud mode. " "Check order is managed server-side."
|
|
391
|
+
)
|
|
41
392
|
|
|
42
393
|
if source < 0 or source >= len(self._checks):
|
|
43
394
|
raise RecceException("Failed to reorder checks. Source index out of range")
|
|
@@ -49,7 +400,74 @@ class CheckDAO:
|
|
|
49
400
|
self._checks.insert(destination, check_to_move)
|
|
50
401
|
|
|
51
402
|
def clear(self):
|
|
403
|
+
"""
|
|
404
|
+
Clear all checks.
|
|
405
|
+
|
|
406
|
+
Note: This operation is only supported in local mode.
|
|
407
|
+
In cloud mode, this is a no-op with a warning.
|
|
408
|
+
"""
|
|
409
|
+
if self.is_cloud_user:
|
|
410
|
+
logger.warning("Clear operation is not supported in cloud mode")
|
|
411
|
+
return
|
|
412
|
+
|
|
52
413
|
self._checks.clear()
|
|
53
414
|
|
|
415
|
+
def mark_as_preset_check(self, check_id: UUID, order_idx: int = 0) -> None:
|
|
416
|
+
"""
|
|
417
|
+
Mark a check as a preset check.
|
|
418
|
+
|
|
419
|
+
This operation is only supported for cloud users. It creates a preset check
|
|
420
|
+
from an existing check, which can then be used across projects.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
check_id: Check ID (UUID)
|
|
424
|
+
order_idx: Order index for the preset check (default: 0)
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
None
|
|
428
|
+
|
|
429
|
+
Raises:
|
|
430
|
+
RecceException: If operation is attempted in local mode or if check not found
|
|
431
|
+
"""
|
|
432
|
+
if not self.is_cloud_user:
|
|
433
|
+
raise RecceException(
|
|
434
|
+
"Marking checks as preset is only supported in cloud mode. This feature requires Recce Cloud."
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Get the original check
|
|
438
|
+
check = self.find_check_by_id(check_id)
|
|
439
|
+
if check is None:
|
|
440
|
+
raise RecceException(f"Check {check_id} not found")
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
org_id, project_id, session_id = self._get_session_info()
|
|
444
|
+
cloud_client = self._get_cloud_client()
|
|
445
|
+
|
|
446
|
+
# Prepare preset check data
|
|
447
|
+
preset_data = {
|
|
448
|
+
"name": check.name,
|
|
449
|
+
"description": check.description if check.description else None,
|
|
450
|
+
"type": check.type.value,
|
|
451
|
+
"params": check.params if check.params else {},
|
|
452
|
+
"view_options": check.view_options if check.view_options else None,
|
|
453
|
+
"order_index": order_idx, # Order index for the preset check
|
|
454
|
+
"check_id": str(check_id),
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
# Create preset check via cloud API
|
|
458
|
+
cloud_client.create_preset_check(org_id, project_id, preset_data)
|
|
459
|
+
|
|
460
|
+
logger.debug(f"Created preset check from check {check_id}")
|
|
461
|
+
except Exception as e:
|
|
462
|
+
logger.error(f"Failed to mark check {check_id} as preset: {e}")
|
|
463
|
+
raise RecceException(f"Failed to create preset check: {e}")
|
|
464
|
+
|
|
54
465
|
def status(self):
|
|
55
|
-
|
|
466
|
+
"""
|
|
467
|
+
Get check statistics.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
dict: Dictionary with 'total' and 'approved' counts
|
|
471
|
+
"""
|
|
472
|
+
checks = self.list()
|
|
473
|
+
return {"total": len(checks), "approved": len([c for c in checks if c.is_checked])}
|
recce/models/types.py
CHANGED
|
@@ -5,6 +5,8 @@ from typing import Dict, List, Literal, Optional, Set
|
|
|
5
5
|
|
|
6
6
|
from pydantic import UUID4, BaseModel, Field
|
|
7
7
|
|
|
8
|
+
from recce.util.pydantic_model import pydantic_model_dump
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
class RunType(Enum):
|
|
10
12
|
SIMPLE = "simple"
|
|
@@ -36,8 +38,6 @@ class RunStatus(Enum):
|
|
|
36
38
|
FAILED = "failed"
|
|
37
39
|
CANCELLED = "cancelled"
|
|
38
40
|
RUNNING = "running"
|
|
39
|
-
# This is a special status only in v0.36.0. Replaced by FINISHED. To be removed in the future.
|
|
40
|
-
SUCCESSFUL = "successful"
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
class Run(BaseModel):
|
|
@@ -52,6 +52,39 @@ class Run(BaseModel):
|
|
|
52
52
|
run_id: UUID4 = Field(default_factory=uuid.uuid4)
|
|
53
53
|
run_at: str = Field(default_factory=lambda: datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"))
|
|
54
54
|
|
|
55
|
+
def __init__(self, **data):
|
|
56
|
+
type = data.get("type")
|
|
57
|
+
|
|
58
|
+
if "result" in data and data["result"] is not None:
|
|
59
|
+
result = data.get("result")
|
|
60
|
+
|
|
61
|
+
if type in [RunType.QUERY.value, RunType.QUERY_BASE.value]:
|
|
62
|
+
from recce.tasks.query import QueryResult
|
|
63
|
+
|
|
64
|
+
data["result"] = pydantic_model_dump(QueryResult(**result))
|
|
65
|
+
elif type == RunType.QUERY_DIFF.value:
|
|
66
|
+
from recce.tasks.query import QueryDiffResult
|
|
67
|
+
|
|
68
|
+
data["result"] = pydantic_model_dump(QueryDiffResult(**result))
|
|
69
|
+
elif type == RunType.PROFILE.value:
|
|
70
|
+
from recce.tasks.profile import ProfileResult
|
|
71
|
+
|
|
72
|
+
data["result"] = pydantic_model_dump(ProfileResult(**result))
|
|
73
|
+
elif type == RunType.PROFILE_DIFF.value:
|
|
74
|
+
from recce.tasks.profile import ProfileDiffResult
|
|
75
|
+
|
|
76
|
+
data["result"] = pydantic_model_dump(ProfileDiffResult(**result))
|
|
77
|
+
elif type == RunType.VALUE_DIFF.value:
|
|
78
|
+
from recce.tasks.valuediff import ValueDiffResult
|
|
79
|
+
|
|
80
|
+
data["result"] = pydantic_model_dump(ValueDiffResult(**result))
|
|
81
|
+
elif type == RunType.VALUE_DIFF_DETAIL.value:
|
|
82
|
+
from recce.tasks.valuediff import ValueDiffDetailResult
|
|
83
|
+
|
|
84
|
+
data["result"] = pydantic_model_dump(ValueDiffDetailResult(**result))
|
|
85
|
+
|
|
86
|
+
super().__init__(**data)
|
|
87
|
+
|
|
55
88
|
|
|
56
89
|
class Check(BaseModel):
|
|
57
90
|
name: str
|
|
@@ -60,8 +93,11 @@ class Check(BaseModel):
|
|
|
60
93
|
params: Optional[dict] = {}
|
|
61
94
|
view_options: Optional[dict] = {}
|
|
62
95
|
check_id: UUID4 = Field(default_factory=uuid.uuid4)
|
|
96
|
+
session_id: Optional[UUID4] = Field(default=None)
|
|
63
97
|
is_checked: bool = False
|
|
64
98
|
is_preset: bool = False
|
|
99
|
+
created_by: Optional[str] = None
|
|
100
|
+
updated_by: Optional[str] = None
|
|
65
101
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc).replace(microsecond=0))
|
|
66
102
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc).replace(microsecond=0))
|
|
67
103
|
|
|
@@ -151,6 +187,29 @@ class CllNode(BaseModel):
|
|
|
151
187
|
# Column to column dependencies
|
|
152
188
|
columns: Dict[str, CllColumn] = Field(default_factory=dict)
|
|
153
189
|
|
|
190
|
+
# If the node is impacted. Only used if option 'change_analysis' is set
|
|
191
|
+
impacted: Optional[bool] = None
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def build_cll_node(cls, manifest, resource_key, node_id) -> Optional["CllNode"]:
|
|
195
|
+
resources = getattr(manifest, resource_key)
|
|
196
|
+
if node_id not in resources:
|
|
197
|
+
return None
|
|
198
|
+
n = resources[node_id]
|
|
199
|
+
if resource_key == "nodes" and n.resource_type not in ["model", "seed", "snapshot"]:
|
|
200
|
+
return None
|
|
201
|
+
cll_node = CllNode(
|
|
202
|
+
id=n.unique_id,
|
|
203
|
+
name=n.name,
|
|
204
|
+
package_name=n.package_name,
|
|
205
|
+
resource_type=n.resource_type,
|
|
206
|
+
)
|
|
207
|
+
if resource_key == "sources":
|
|
208
|
+
cll_node.source_name = n.source_name
|
|
209
|
+
elif resource_key == "nodes":
|
|
210
|
+
cll_node.raw_code = n.raw_code
|
|
211
|
+
return cll_node
|
|
212
|
+
|
|
154
213
|
|
|
155
214
|
class CllData(BaseModel):
|
|
156
215
|
nodes: Dict[str, CllNode] = Field(default_factory=dict)
|
recce/pull_request.py
CHANGED
|
@@ -83,7 +83,7 @@ def fetch_pr_metadata_from_event_path() -> Optional[dict]:
|
|
|
83
83
|
github_repository = os.getenv("GITHUB_REPOSITORY")
|
|
84
84
|
if event_path:
|
|
85
85
|
try:
|
|
86
|
-
with open(event_path, "r") as event_file:
|
|
86
|
+
with open(event_path, "r", encoding="utf-8") as event_file:
|
|
87
87
|
event_data = json.load(event_file)
|
|
88
88
|
|
|
89
89
|
pr_id = event_data["number"]
|