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/apis/check_api.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from datetime import datetime, timezone
|
|
2
1
|
from typing import Optional
|
|
3
2
|
from uuid import UUID
|
|
4
3
|
|
|
@@ -153,22 +152,10 @@ class PatchCheckIn(BaseModel):
|
|
|
153
152
|
|
|
154
153
|
@check_router.patch("/checks/{check_id}", status_code=200, response_model=CheckOut, response_model_exclude_none=True)
|
|
155
154
|
async def update_check_handler(check_id: UUID, patch: PatchCheckIn, background_tasks: BackgroundTasks):
|
|
156
|
-
check = CheckDAO().
|
|
155
|
+
check = CheckDAO().update_check_by_id(check_id, patch)
|
|
157
156
|
if check is None:
|
|
158
157
|
raise HTTPException(status_code=404, detail="Not Found")
|
|
159
158
|
|
|
160
|
-
if patch.name is not None:
|
|
161
|
-
check.name = patch.name
|
|
162
|
-
if patch.description is not None:
|
|
163
|
-
check.description = patch.description
|
|
164
|
-
if patch.params is not None:
|
|
165
|
-
check.params = patch.params
|
|
166
|
-
if patch.view_options is not None:
|
|
167
|
-
check.view_options = patch.view_options
|
|
168
|
-
if patch.is_checked is not None:
|
|
169
|
-
check.is_checked = patch.is_checked
|
|
170
|
-
check.updated_at = datetime.now(timezone.utc).replace(microsecond=0)
|
|
171
|
-
|
|
172
159
|
background_tasks.add_task(export_persistent_state)
|
|
173
160
|
return CheckOut.from_check(check)
|
|
174
161
|
|
|
@@ -195,3 +182,22 @@ async def reorder_handler(order: ReorderChecksIn):
|
|
|
195
182
|
CheckDAO().reorder(order.source, order.destination)
|
|
196
183
|
except RecceException as e:
|
|
197
184
|
raise HTTPException(status_code=400, detail=e.message)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@check_router.post("/checks/{check_id}/mark-as-preset", status_code=204)
|
|
188
|
+
async def mark_as_preset_check_handler(check_id: UUID, background_tasks: BackgroundTasks):
|
|
189
|
+
"""
|
|
190
|
+
Mark an existing check as a preset check (cloud users only).
|
|
191
|
+
|
|
192
|
+
This creates a preset check from the specified check.
|
|
193
|
+
Only available for users with cloud mode enabled.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
204 No Content: Successfully marked check as preset
|
|
197
|
+
400 Bad Request: Error with detail message (e.g., not in cloud mode, check not found)
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
CheckDAO().mark_as_preset_check(check_id)
|
|
201
|
+
background_tasks.add_task(export_persistent_state)
|
|
202
|
+
except RecceException as e:
|
|
203
|
+
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check Events API endpoints.
|
|
3
|
+
|
|
4
|
+
This module provides REST endpoints for check events (timeline/conversation),
|
|
5
|
+
proxying requests to Recce Cloud. This feature is only available for cloud users.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from recce.core import default_context
|
|
16
|
+
from recce.event import get_recce_api_token
|
|
17
|
+
from recce.exceptions import RecceException
|
|
18
|
+
from recce.util.cloud.check_events import CheckEventsCloud
|
|
19
|
+
from recce.util.recce_cloud import RecceCloud, RecceCloudException
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("uvicorn")
|
|
22
|
+
|
|
23
|
+
check_events_router = APIRouter(tags=["check_events"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ============================================================================
|
|
27
|
+
# Helper Functions
|
|
28
|
+
# ============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_cloud_user() -> bool:
|
|
32
|
+
"""Check if the current user is connected to Recce Cloud."""
|
|
33
|
+
ctx = default_context()
|
|
34
|
+
if ctx is None or ctx.state_loader is None:
|
|
35
|
+
return False
|
|
36
|
+
return hasattr(ctx.state_loader, "session_id") and ctx.state_loader.session_id is not None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_session_info() -> tuple:
|
|
40
|
+
"""
|
|
41
|
+
Get organization ID, project ID, and session ID from state loader.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
tuple: (org_id, project_id, session_id)
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
HTTPException: If not in cloud mode or session info unavailable
|
|
48
|
+
"""
|
|
49
|
+
if not _is_cloud_user():
|
|
50
|
+
raise HTTPException(
|
|
51
|
+
status_code=400,
|
|
52
|
+
detail="Check events are only available when connected to Recce Cloud.",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
ctx = default_context()
|
|
56
|
+
state_loader = ctx.state_loader
|
|
57
|
+
|
|
58
|
+
session_id = state_loader.session_id
|
|
59
|
+
|
|
60
|
+
# Check if org_id and project_id are cached
|
|
61
|
+
if hasattr(state_loader, "org_id") and hasattr(state_loader, "project_id"):
|
|
62
|
+
return state_loader.org_id, state_loader.project_id, session_id
|
|
63
|
+
|
|
64
|
+
# Fetch from cloud API
|
|
65
|
+
api_token = get_recce_api_token() or state_loader.token
|
|
66
|
+
if not api_token:
|
|
67
|
+
raise HTTPException(
|
|
68
|
+
status_code=401,
|
|
69
|
+
detail="Cannot access Recce Cloud: no API token available.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
recce_cloud = RecceCloud(api_token)
|
|
74
|
+
session = recce_cloud.get_session(session_id)
|
|
75
|
+
|
|
76
|
+
org_id = session.get("org_id")
|
|
77
|
+
project_id = session.get("project_id")
|
|
78
|
+
|
|
79
|
+
if not org_id or not project_id:
|
|
80
|
+
raise HTTPException(
|
|
81
|
+
status_code=400,
|
|
82
|
+
detail=f"Session {session_id} does not belong to a valid organization or project.",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Cache for future use
|
|
86
|
+
state_loader.org_id = org_id
|
|
87
|
+
state_loader.project_id = project_id
|
|
88
|
+
|
|
89
|
+
return org_id, project_id, session_id
|
|
90
|
+
|
|
91
|
+
except RecceCloudException as e:
|
|
92
|
+
logger.error(f"Failed to get session info: {e}")
|
|
93
|
+
raise HTTPException(status_code=e.status_code, detail=str(e.reason))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _get_events_client() -> CheckEventsCloud:
|
|
97
|
+
"""
|
|
98
|
+
Get the CheckEventsCloud client.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
CheckEventsCloud: Cloud client for event operations
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
HTTPException: If client cannot be initialized
|
|
105
|
+
"""
|
|
106
|
+
ctx = default_context()
|
|
107
|
+
api_token = get_recce_api_token() or ctx.state_loader.token
|
|
108
|
+
|
|
109
|
+
if not api_token:
|
|
110
|
+
raise HTTPException(
|
|
111
|
+
status_code=401,
|
|
112
|
+
detail="Cannot access Recce Cloud: no API token available.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return CheckEventsCloud(api_token)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ============================================================================
|
|
119
|
+
# Pydantic Models
|
|
120
|
+
# ============================================================================
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CheckEventActorOut(BaseModel):
|
|
124
|
+
"""Actor who performed the event."""
|
|
125
|
+
|
|
126
|
+
type: str # "user", "recce_ai", "preset_system"
|
|
127
|
+
user_id: Optional[int] = None
|
|
128
|
+
login: Optional[str] = None
|
|
129
|
+
fullname: Optional[str] = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class CheckEventOut(BaseModel):
|
|
133
|
+
"""Check event response model."""
|
|
134
|
+
|
|
135
|
+
id: str
|
|
136
|
+
check_id: str
|
|
137
|
+
event_type: str
|
|
138
|
+
actor: CheckEventActorOut
|
|
139
|
+
content: Optional[str] = None
|
|
140
|
+
old_value: Optional[str] = None
|
|
141
|
+
new_value: Optional[str] = None
|
|
142
|
+
is_edited: bool = False
|
|
143
|
+
is_deleted: bool = False
|
|
144
|
+
created_at: str
|
|
145
|
+
updated_at: str
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class CreateCommentIn(BaseModel):
|
|
149
|
+
"""Request body for creating a comment."""
|
|
150
|
+
|
|
151
|
+
content: str
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class UpdateCommentIn(BaseModel):
|
|
155
|
+
"""Request body for updating a comment."""
|
|
156
|
+
|
|
157
|
+
content: str
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ============================================================================
|
|
161
|
+
# API Endpoints
|
|
162
|
+
# ============================================================================
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@check_events_router.get(
|
|
166
|
+
"/checks/{check_id}/events",
|
|
167
|
+
status_code=200,
|
|
168
|
+
response_model=List[CheckEventOut],
|
|
169
|
+
)
|
|
170
|
+
async def list_check_events(check_id: UUID):
|
|
171
|
+
"""
|
|
172
|
+
List all events for a check in chronological order.
|
|
173
|
+
|
|
174
|
+
This endpoint returns all events (comments, state changes, etc.) for the
|
|
175
|
+
specified check. Events are returned in chronological order (oldest first).
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
check_id: The check ID
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of CheckEventOut objects
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
400: Not connected to Recce Cloud
|
|
185
|
+
401: No API token available
|
|
186
|
+
404: Check not found
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
org_id, project_id, session_id = _get_session_info()
|
|
190
|
+
client = _get_events_client()
|
|
191
|
+
|
|
192
|
+
events = client.list_events(org_id, project_id, session_id, str(check_id))
|
|
193
|
+
return events
|
|
194
|
+
|
|
195
|
+
except RecceCloudException as e:
|
|
196
|
+
logger.error(f"Failed to list check events: {e}")
|
|
197
|
+
raise HTTPException(status_code=e.status_code, detail=str(e.reason))
|
|
198
|
+
except RecceException as e:
|
|
199
|
+
logger.error(f"Failed to list check events: {e}")
|
|
200
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@check_events_router.get(
|
|
204
|
+
"/checks/{check_id}/events/{event_id}",
|
|
205
|
+
status_code=200,
|
|
206
|
+
response_model=CheckEventOut,
|
|
207
|
+
)
|
|
208
|
+
async def get_check_event(check_id: UUID, event_id: UUID):
|
|
209
|
+
"""
|
|
210
|
+
Get a specific event by ID.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
check_id: The check ID
|
|
214
|
+
event_id: The event ID
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
CheckEventOut object
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
400: Not connected to Recce Cloud
|
|
221
|
+
401: No API token available
|
|
222
|
+
404: Event not found
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
org_id, project_id, session_id = _get_session_info()
|
|
226
|
+
client = _get_events_client()
|
|
227
|
+
|
|
228
|
+
event = client.get_event(org_id, project_id, session_id, str(check_id), str(event_id))
|
|
229
|
+
return event
|
|
230
|
+
|
|
231
|
+
except RecceCloudException as e:
|
|
232
|
+
logger.error(f"Failed to get check event: {e}")
|
|
233
|
+
raise HTTPException(status_code=e.status_code, detail=str(e.reason))
|
|
234
|
+
except RecceException as e:
|
|
235
|
+
logger.error(f"Failed to get check event: {e}")
|
|
236
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@check_events_router.post(
|
|
240
|
+
"/checks/{check_id}/events",
|
|
241
|
+
status_code=201,
|
|
242
|
+
response_model=CheckEventOut,
|
|
243
|
+
)
|
|
244
|
+
async def create_comment(check_id: UUID, body: CreateCommentIn):
|
|
245
|
+
"""
|
|
246
|
+
Create a new comment on a check.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
check_id: The check ID
|
|
250
|
+
body: Request body containing comment content
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Created CheckEventOut object
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
400: Not connected to Recce Cloud or invalid content
|
|
257
|
+
401: No API token available
|
|
258
|
+
404: Check not found
|
|
259
|
+
"""
|
|
260
|
+
if not body.content or not body.content.strip():
|
|
261
|
+
raise HTTPException(status_code=400, detail="Comment content cannot be empty.")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
org_id, project_id, session_id = _get_session_info()
|
|
265
|
+
client = _get_events_client()
|
|
266
|
+
|
|
267
|
+
event = client.create_comment(org_id, project_id, session_id, str(check_id), body.content)
|
|
268
|
+
return event
|
|
269
|
+
|
|
270
|
+
except RecceCloudException as e:
|
|
271
|
+
logger.error(f"Failed to create comment: {e}")
|
|
272
|
+
raise HTTPException(status_code=e.status_code, detail=str(e.reason))
|
|
273
|
+
except RecceException as e:
|
|
274
|
+
logger.error(f"Failed to create comment: {e}")
|
|
275
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@check_events_router.patch(
|
|
279
|
+
"/checks/{check_id}/events/{event_id}",
|
|
280
|
+
status_code=200,
|
|
281
|
+
response_model=CheckEventOut,
|
|
282
|
+
)
|
|
283
|
+
async def update_comment(check_id: UUID, event_id: UUID, body: UpdateCommentIn):
|
|
284
|
+
"""
|
|
285
|
+
Update an existing comment.
|
|
286
|
+
|
|
287
|
+
Only the author or an admin can update a comment.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
check_id: The check ID
|
|
291
|
+
event_id: The event ID of the comment to update
|
|
292
|
+
body: Request body containing new comment content
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Updated CheckEventOut object
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
400: Not connected to Recce Cloud or invalid content
|
|
299
|
+
401: No API token available
|
|
300
|
+
403: Not authorized to update this comment
|
|
301
|
+
404: Comment not found
|
|
302
|
+
"""
|
|
303
|
+
if not body.content or not body.content.strip():
|
|
304
|
+
raise HTTPException(status_code=400, detail="Comment content cannot be empty.")
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
org_id, project_id, session_id = _get_session_info()
|
|
308
|
+
client = _get_events_client()
|
|
309
|
+
|
|
310
|
+
event = client.update_comment(org_id, project_id, session_id, str(check_id), str(event_id), body.content)
|
|
311
|
+
return event
|
|
312
|
+
|
|
313
|
+
except RecceCloudException as e:
|
|
314
|
+
logger.error(f"Failed to update comment: {e}")
|
|
315
|
+
raise HTTPException(status_code=e.status_code, detail=str(e.reason))
|
|
316
|
+
except RecceException as e:
|
|
317
|
+
logger.error(f"Failed to update comment: {e}")
|
|
318
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@check_events_router.delete(
|
|
322
|
+
"/checks/{check_id}/events/{event_id}",
|
|
323
|
+
status_code=204,
|
|
324
|
+
)
|
|
325
|
+
async def delete_comment(check_id: UUID, event_id: UUID):
|
|
326
|
+
"""
|
|
327
|
+
Delete a comment (soft delete).
|
|
328
|
+
|
|
329
|
+
Only the author or an admin can delete a comment. The comment will be
|
|
330
|
+
marked as deleted but remain in the timeline with a "Comment deleted" indicator.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
check_id: The check ID
|
|
334
|
+
event_id: The event ID of the comment to delete
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
400: Not connected to Recce Cloud
|
|
338
|
+
401: No API token available
|
|
339
|
+
403: Not authorized to delete this comment
|
|
340
|
+
404: Comment not found
|
|
341
|
+
"""
|
|
342
|
+
try:
|
|
343
|
+
org_id, project_id, session_id = _get_session_info()
|
|
344
|
+
client = _get_events_client()
|
|
345
|
+
|
|
346
|
+
client.delete_comment(org_id, project_id, session_id, str(check_id), str(event_id))
|
|
347
|
+
|
|
348
|
+
except RecceCloudException as e:
|
|
349
|
+
logger.error(f"Failed to delete comment: {e}")
|
|
350
|
+
raise HTTPException(status_code=e.status_code, detail=str(e.reason))
|
|
351
|
+
except RecceException as e:
|
|
352
|
+
logger.error(f"Failed to delete comment: {e}")
|
|
353
|
+
raise HTTPException(status_code=400, detail=str(e))
|
recce/apis/check_func.py
CHANGED
|
@@ -86,10 +86,10 @@ def create_check_from_run(
|
|
|
86
86
|
is_preset=is_preset,
|
|
87
87
|
is_checked=is_checked,
|
|
88
88
|
)
|
|
89
|
-
CheckDAO().create(check)
|
|
90
|
-
run.check_id =
|
|
89
|
+
new_check = CheckDAO().create(check)
|
|
90
|
+
run.check_id = new_check.check_id
|
|
91
91
|
|
|
92
|
-
return
|
|
92
|
+
return new_check
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def create_check_without_run(
|
|
@@ -105,8 +105,8 @@ def create_check_without_run(
|
|
|
105
105
|
is_preset=is_preset,
|
|
106
106
|
is_checked=is_checked,
|
|
107
107
|
)
|
|
108
|
-
CheckDAO().create(check)
|
|
109
|
-
return
|
|
108
|
+
new_check = CheckDAO().create(check)
|
|
109
|
+
return new_check
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
def purge_preset_checks():
|
recce/apis/run_func.py
CHANGED
|
@@ -122,12 +122,16 @@ def submit_run(type, params, check_id=None):
|
|
|
122
122
|
|
|
123
123
|
task.progress_listener = progress_listener
|
|
124
124
|
|
|
125
|
-
async def update_run_result(
|
|
125
|
+
async def update_run_result(run, result, error, updated_params=None):
|
|
126
|
+
"""Update run with result, error, and optionally updated params."""
|
|
126
127
|
if run is None:
|
|
127
128
|
return
|
|
128
129
|
if result is not None:
|
|
129
130
|
run.result = result
|
|
130
131
|
run.status = RunStatus.FINISHED
|
|
132
|
+
if updated_params is not None:
|
|
133
|
+
# Merge updated params (preserves any fields not in updated_params)
|
|
134
|
+
run.params.update(updated_params)
|
|
131
135
|
if error is not None:
|
|
132
136
|
failed_reason = str(error) if str(error) != "None" else repr(error)
|
|
133
137
|
run.error = failed_reason
|
|
@@ -138,10 +142,35 @@ def submit_run(type, params, check_id=None):
|
|
|
138
142
|
def fn():
|
|
139
143
|
try:
|
|
140
144
|
result = task.execute()
|
|
141
|
-
|
|
145
|
+
|
|
146
|
+
# Extract updated params from task after execution
|
|
147
|
+
updated_params = None
|
|
148
|
+
if hasattr(task, "params") and task.params is not None:
|
|
149
|
+
# Serialization logic:
|
|
150
|
+
# - Most tasks use Pydantic models (v2: model_dump, v1: dict)
|
|
151
|
+
# - Some tasks may use plain dicts
|
|
152
|
+
# - If params is an unexpected type, log a warning for debugging
|
|
153
|
+
# - Handle the case where model_dump() or dict() raises an exception.
|
|
154
|
+
try:
|
|
155
|
+
if hasattr(task.params, "model_dump"):
|
|
156
|
+
updated_params = task.params.model_dump()
|
|
157
|
+
elif hasattr(task.params, "dict"):
|
|
158
|
+
updated_params = task.params.dict()
|
|
159
|
+
elif isinstance(task.params, dict):
|
|
160
|
+
updated_params = task.params
|
|
161
|
+
else:
|
|
162
|
+
logger.warning(
|
|
163
|
+
f"Could not serialize task.params for run_id={run.run_id}: "
|
|
164
|
+
f"unexpected type {type(task.params)} with value {repr(task.params)}"
|
|
165
|
+
)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.warning(f"Failed to serialize task.params: {e}")
|
|
168
|
+
updated_params = None
|
|
169
|
+
|
|
170
|
+
asyncio.run_coroutine_threadsafe(update_run_result(run, result, None, updated_params), loop)
|
|
142
171
|
return result
|
|
143
172
|
except BaseException as e:
|
|
144
|
-
asyncio.run_coroutine_threadsafe(update_run_result(run
|
|
173
|
+
asyncio.run_coroutine_threadsafe(update_run_result(run, None, e, None), loop)
|
|
145
174
|
if isinstance(e, RecceException) and e.is_raise is False:
|
|
146
175
|
return None
|
|
147
176
|
import sentry_sdk
|
recce/artifact.py
CHANGED
|
@@ -40,7 +40,7 @@ def verify_artifacts_path(target_path: str) -> bool:
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def parse_dbt_version(file_path: str) -> str:
|
|
43
|
-
with open(file_path, "r") as f:
|
|
43
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
44
44
|
data = json.load(f)
|
|
45
45
|
|
|
46
46
|
dbt_version = data.get("metadata", {}).get("dbt_version", None)
|
|
@@ -80,6 +80,64 @@ def archive_artifacts(target_path: str) -> (str, str):
|
|
|
80
80
|
return artifacts_tar_gz_path, dbt_version
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
def upload_artifacts_to_session(target_path: str, session_id: str, token: str, debug: bool = False):
|
|
84
|
+
"""Upload dbt artifacts to a specific session ID in Recce Cloud."""
|
|
85
|
+
console = Console()
|
|
86
|
+
if verify_artifacts_path(target_path) is False:
|
|
87
|
+
console.print(f"[[red]Error[/red]] Invalid target path: {target_path}")
|
|
88
|
+
console.print("Please provide a valid target path containing manifest.json and catalog.json.")
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
manifest_path = os.path.join(target_path, "manifest.json")
|
|
92
|
+
catalog_path = os.path.join(target_path, "catalog.json")
|
|
93
|
+
|
|
94
|
+
# get the adapter type from the manifest file
|
|
95
|
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
96
|
+
manifest_data = json.load(f)
|
|
97
|
+
adapter_type = manifest_data.get("metadata", {}).get("adapter_type")
|
|
98
|
+
if adapter_type is None:
|
|
99
|
+
raise Exception("Failed to parse adapter type from manifest.json")
|
|
100
|
+
|
|
101
|
+
recce_cloud = RecceCloud(token)
|
|
102
|
+
|
|
103
|
+
session = recce_cloud.get_session(session_id)
|
|
104
|
+
|
|
105
|
+
org_id = session.get("org_id")
|
|
106
|
+
if org_id is None:
|
|
107
|
+
raise Exception(f"Session ID {session_id} does not belong to any organization.")
|
|
108
|
+
|
|
109
|
+
project_id = session.get("project_id")
|
|
110
|
+
if project_id is None:
|
|
111
|
+
raise Exception(f"Session ID {session_id} does not belong to any project.")
|
|
112
|
+
|
|
113
|
+
# Get the presigned URL for uploading the artifacts using session ID
|
|
114
|
+
console.print(f'Uploading artifacts for session ID "{session_id}"')
|
|
115
|
+
presigned_urls = recce_cloud.get_upload_urls_by_session_id(org_id, project_id, session_id)
|
|
116
|
+
if debug:
|
|
117
|
+
console.rule("Debug information", style="blue")
|
|
118
|
+
console.print(f"Org ID: {org_id}")
|
|
119
|
+
console.print(f"Project ID: {project_id}")
|
|
120
|
+
console.print(f"Session ID: {session_id}")
|
|
121
|
+
console.print(f"Manifest path: {presigned_urls['manifest_url']}")
|
|
122
|
+
console.print(f"Catalog path: {presigned_urls['catalog_url']}")
|
|
123
|
+
console.print(f"Adapter type: {adapter_type}")
|
|
124
|
+
|
|
125
|
+
# Upload the compressed artifacts (no password needed for session uploads)
|
|
126
|
+
console.print(f'Uploading manifest from path "{manifest_path}"')
|
|
127
|
+
response = requests.put(presigned_urls["manifest_url"], data=open(manifest_path, "rb").read())
|
|
128
|
+
if response.status_code != 200 and response.status_code != 204:
|
|
129
|
+
raise Exception(response.text)
|
|
130
|
+
console.print(f'Uploading catalog from path "{catalog_path}"')
|
|
131
|
+
response = requests.put(presigned_urls["catalog_url"], data=open(catalog_path, "rb").read())
|
|
132
|
+
if response.status_code != 200 and response.status_code != 204:
|
|
133
|
+
raise Exception(response.text)
|
|
134
|
+
|
|
135
|
+
# Update the session metadata
|
|
136
|
+
recce_cloud.update_session(org_id, project_id, session_id, adapter_type)
|
|
137
|
+
|
|
138
|
+
return 0
|
|
139
|
+
|
|
140
|
+
|
|
83
141
|
def upload_dbt_artifacts(target_path: str, branch: str, token: str, password: str, debug: bool = False):
|
|
84
142
|
console = Console()
|
|
85
143
|
if verify_artifacts_path(target_path) is False:
|
|
@@ -100,7 +158,7 @@ def upload_dbt_artifacts(target_path: str, branch: str, token: str, password: st
|
|
|
100
158
|
metadata = {"commit": sha, "dbt_version": dbt_version}
|
|
101
159
|
|
|
102
160
|
# Get the presigned URL for uploading the artifacts
|
|
103
|
-
presigned_url = RecceCloud(token).
|
|
161
|
+
presigned_url = RecceCloud(token).get_presigned_url_by_github_repo(
|
|
104
162
|
method=PresignedUrlMethod.UPLOAD,
|
|
105
163
|
repository=repo,
|
|
106
164
|
artifact_name="dbt_artifacts.tar.gz",
|
|
@@ -145,7 +203,7 @@ def download_dbt_artifacts(
|
|
|
145
203
|
sha = None
|
|
146
204
|
dbt_version = None
|
|
147
205
|
|
|
148
|
-
presigned_url, tags = RecceCloud(token).
|
|
206
|
+
presigned_url, tags = RecceCloud(token).get_download_presigned_url_by_github_repo_with_tags(
|
|
149
207
|
repository=repo,
|
|
150
208
|
artifact_name="dbt_artifacts.tar.gz",
|
|
151
209
|
branch=branch,
|
|
@@ -191,3 +249,18 @@ def download_dbt_artifacts(
|
|
|
191
249
|
except FileNotFoundError:
|
|
192
250
|
pass
|
|
193
251
|
return 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def delete_dbt_artifacts(branch: str, token: str, debug: bool = False):
|
|
255
|
+
"""Delete dbt artifacts from a specific branch in Recce Cloud."""
|
|
256
|
+
console = Console()
|
|
257
|
+
repo = hosting_repo()
|
|
258
|
+
|
|
259
|
+
if debug:
|
|
260
|
+
console.rule("Debug information", style="blue")
|
|
261
|
+
console.print(f"Git Branch: {branch}")
|
|
262
|
+
console.print(f"GitHub repository: {repo}")
|
|
263
|
+
|
|
264
|
+
console.print(f'Deleting dbt artifacts from branch: "{branch}"')
|
|
265
|
+
|
|
266
|
+
RecceCloud(token).purge_artifacts(repo, branch=branch)
|