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
|
@@ -2,6 +2,7 @@ class DbtVersion:
|
|
|
2
2
|
|
|
3
3
|
def __init__(self):
|
|
4
4
|
from dbt import version as dbt_version
|
|
5
|
+
|
|
5
6
|
dbt_version = self.parse(dbt_version.__version__)
|
|
6
7
|
if dbt_version.is_prerelease:
|
|
7
8
|
dbt_version = self.parse(dbt_version.base_version)
|
|
@@ -10,10 +11,12 @@ class DbtVersion:
|
|
|
10
11
|
@staticmethod
|
|
11
12
|
def parse(version: str):
|
|
12
13
|
from packaging import version as v
|
|
14
|
+
|
|
13
15
|
return v.parse(version)
|
|
14
16
|
|
|
15
17
|
def as_version(self, other):
|
|
16
18
|
from packaging.version import Version
|
|
19
|
+
|
|
17
20
|
if isinstance(other, Version):
|
|
18
21
|
return other
|
|
19
22
|
if isinstance(other, str):
|
recce/adapter/sqlmesh_adapter.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import typing as t
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Dict, Optional, Type
|
|
4
4
|
|
|
5
5
|
import pandas as pd
|
|
6
|
-
from sqlglot import
|
|
6
|
+
from sqlglot import Expression, parse_one, select
|
|
7
7
|
from sqlglot.optimizer import traverse_scope
|
|
8
8
|
from sqlmesh.core.context import Context as SqlmeshContext
|
|
9
9
|
from sqlmesh.core.environment import Environment
|
|
@@ -11,7 +11,7 @@ from sqlmesh.core.state_sync import StateReader
|
|
|
11
11
|
|
|
12
12
|
from recce.adapter.base import BaseAdapter
|
|
13
13
|
from recce.models import RunType
|
|
14
|
-
from recce.tasks import
|
|
14
|
+
from recce.tasks import QueryDiffTask, QueryTask, RowCountDiffTask, Task
|
|
15
15
|
|
|
16
16
|
sqlmesh_supported_registry: Dict[RunType, Type[Task]] = {
|
|
17
17
|
RunType.QUERY: QueryTask,
|
|
@@ -37,7 +37,7 @@ class SqlmeshAdapter(BaseAdapter):
|
|
|
37
37
|
|
|
38
38
|
for snapshot in state_reader.get_snapshots(env.snapshots, hydrate_seeds=True).values():
|
|
39
39
|
|
|
40
|
-
if snapshot.node_type.lower() !=
|
|
40
|
+
if snapshot.node_type.lower() != "model":
|
|
41
41
|
continue
|
|
42
42
|
|
|
43
43
|
model = snapshot.model
|
|
@@ -45,16 +45,16 @@ class SqlmeshAdapter(BaseAdapter):
|
|
|
45
45
|
continue
|
|
46
46
|
|
|
47
47
|
node = {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
"unique_id": model.name,
|
|
49
|
+
"name": model.name,
|
|
50
|
+
"resource_type": snapshot.node_type.lower(),
|
|
51
|
+
"checksum": {"checksum": snapshot.fingerprint.data_hash},
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
columns = {}
|
|
55
55
|
for column, type in model.columns_to_types.items():
|
|
56
|
-
columns[column] = {
|
|
57
|
-
node[
|
|
56
|
+
columns[column] = {"name": column, "type": str(type)}
|
|
57
|
+
node["columns"] = columns
|
|
58
58
|
|
|
59
59
|
nodes[snapshot.name] = node
|
|
60
60
|
parents = [snapshotId.name for snapshotId in snapshot.parents]
|
|
@@ -81,15 +81,15 @@ class SqlmeshAdapter(BaseAdapter):
|
|
|
81
81
|
|
|
82
82
|
@classmethod
|
|
83
83
|
def load(cls, **kwargs):
|
|
84
|
-
sqlmesh_envs = kwargs.get(
|
|
84
|
+
sqlmesh_envs = kwargs.get("sqlmesh_envs")
|
|
85
85
|
if sqlmesh_envs is None:
|
|
86
|
-
raise Exception('
|
|
86
|
+
raise Exception("'--sqlmesh-envs SOURCE:TARGET' is required")
|
|
87
87
|
|
|
88
|
-
envs = sqlmesh_envs.split(
|
|
88
|
+
envs = sqlmesh_envs.split(":")
|
|
89
89
|
if len(envs) != 2:
|
|
90
90
|
raise Exception('sqlmesh_envs must be in the format of "SOURCE:TARGET"')
|
|
91
91
|
|
|
92
|
-
sqlmesh_config = kwargs.get(
|
|
92
|
+
sqlmesh_config = kwargs.get("sqlmesh_config", None)
|
|
93
93
|
context = SqlmeshContext(config=sqlmesh_config)
|
|
94
94
|
base_env = context.state_reader.get_environment(envs[0])
|
|
95
95
|
curr_env = context.state_reader.get_environment(envs[1])
|
|
@@ -100,18 +100,14 @@ class SqlmeshAdapter(BaseAdapter):
|
|
|
100
100
|
|
|
101
101
|
return cls(context=context, base_env=base_env, curr_env=curr_env)
|
|
102
102
|
|
|
103
|
-
def replace_virtual_tables(
|
|
104
|
-
|
|
105
|
-
sql: t.Union[Expression, str],
|
|
106
|
-
base: bool = None
|
|
107
|
-
) -> Expression:
|
|
108
|
-
'''
|
|
103
|
+
def replace_virtual_tables(self, sql: t.Union[Expression, str], base: bool = None) -> Expression:
|
|
104
|
+
"""
|
|
109
105
|
Replace virtual tables based on the env name.
|
|
110
106
|
|
|
111
107
|
Args:
|
|
112
108
|
sql: SQL expression to replace virtual tables
|
|
113
109
|
base: True: replace virtual tables with base env, False: replace virtual tables with current env, None: no replacement
|
|
114
|
-
|
|
110
|
+
"""
|
|
115
111
|
if isinstance(sql, str):
|
|
116
112
|
expression = parse_one(sql, dialect=self.context.default_dialect)
|
|
117
113
|
else:
|
|
@@ -119,30 +115,23 @@ class SqlmeshAdapter(BaseAdapter):
|
|
|
119
115
|
|
|
120
116
|
if base is not None:
|
|
121
117
|
env = self.base_env if base else self.curr_env
|
|
122
|
-
if env.name !=
|
|
118
|
+
if env.name != "prod":
|
|
123
119
|
model_names = [model.name for model in self.context.models.values()]
|
|
124
120
|
for scope in traverse_scope(expression):
|
|
125
121
|
for table in scope.tables:
|
|
126
|
-
if f
|
|
127
|
-
table.args[
|
|
122
|
+
if f"{table.db}.{table.name}" in model_names:
|
|
123
|
+
table.args["db"] = f"{table.args['db']}__{env.name}"
|
|
128
124
|
|
|
129
125
|
return expression
|
|
130
126
|
|
|
131
127
|
def fetchdf_with_limit(
|
|
132
|
-
self,
|
|
133
|
-
sql: t.Union[Expression, str],
|
|
134
|
-
base: Optional[bool] = None,
|
|
135
|
-
limit: Optional[int] = None
|
|
128
|
+
self, sql: t.Union[Expression, str], base: Optional[bool] = None, limit: Optional[int] = None
|
|
136
129
|
) -> (pd.DataFrame, bool):
|
|
137
130
|
expression = self.replace_virtual_tables(sql, base=base)
|
|
138
131
|
if limit:
|
|
139
|
-
expression =
|
|
140
|
-
|
|
141
|
-
)
|
|
142
|
-
'__QUERY'
|
|
143
|
-
).with_(
|
|
144
|
-
'__QUERY', as_=expression
|
|
145
|
-
).limit(limit + 1 if limit else None)
|
|
132
|
+
expression = (
|
|
133
|
+
select("*").from_("__QUERY").with_("__QUERY", as_=expression).limit(limit + 1 if limit else None)
|
|
134
|
+
)
|
|
146
135
|
df = self.context.fetchdf(expression)
|
|
147
136
|
if limit and len(df) > limit:
|
|
148
137
|
df = df.head(limit)
|
recce/apis/check_api.py
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
|
-
from datetime import datetime, timezone
|
|
2
1
|
from typing import Optional
|
|
3
2
|
from uuid import UUID
|
|
4
3
|
|
|
5
|
-
from fastapi import APIRouter,
|
|
4
|
+
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
|
6
5
|
from pydantic import BaseModel
|
|
7
6
|
|
|
8
|
-
from recce.apis.check_func import
|
|
7
|
+
from recce.apis.check_func import (
|
|
8
|
+
create_check_from_run,
|
|
9
|
+
create_check_without_run,
|
|
10
|
+
export_persistent_state,
|
|
11
|
+
)
|
|
9
12
|
from recce.apis.run_func import submit_run
|
|
10
13
|
from recce.event import log_api_event
|
|
11
14
|
from recce.exceptions import RecceException
|
|
12
|
-
from recce.models import
|
|
15
|
+
from recce.models import Check, CheckDAO, Run, RunDAO, RunType
|
|
13
16
|
|
|
14
|
-
check_router = APIRouter(tags=[
|
|
17
|
+
check_router = APIRouter(tags=["check"])
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
class CreateCheckIn(BaseModel):
|
|
18
21
|
name: Optional[str] = None
|
|
19
|
-
description: str =
|
|
22
|
+
description: str = ""
|
|
20
23
|
run_id: Optional[str] = None
|
|
21
|
-
type: Optional[RunType] = None,
|
|
24
|
+
type: Optional[RunType] = (None,)
|
|
22
25
|
params: Optional[dict] = None
|
|
23
26
|
view_options: Optional[dict] = None
|
|
24
27
|
track_props: Optional[dict] = None
|
|
@@ -39,16 +42,17 @@ class CheckOut(BaseModel):
|
|
|
39
42
|
def from_check(cls, check: Check):
|
|
40
43
|
check_related_runs = RunDAO().list_by_check_id(check.check_id)
|
|
41
44
|
last_run = check_related_runs[-1] if len(check_related_runs) > 0 else None
|
|
42
|
-
return CheckOut(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
return CheckOut(
|
|
46
|
+
check_id=check.check_id,
|
|
47
|
+
name=check.name,
|
|
48
|
+
description=check.description,
|
|
49
|
+
type=check.type.value,
|
|
50
|
+
params=check.params,
|
|
51
|
+
view_options=check.view_options,
|
|
52
|
+
is_checked=check.is_checked,
|
|
53
|
+
is_preset=check.is_preset,
|
|
54
|
+
last_run=last_run,
|
|
55
|
+
)
|
|
52
56
|
|
|
53
57
|
|
|
54
58
|
@check_router.post("/checks", status_code=201, response_model=CheckOut)
|
|
@@ -59,7 +63,7 @@ async def create_check(check_in: CreateCheckIn, background_tasks: BackgroundTask
|
|
|
59
63
|
check_in.run_id,
|
|
60
64
|
check_name=check_in.name,
|
|
61
65
|
check_description=check_in.description,
|
|
62
|
-
check_view_options=check_in.view_options
|
|
66
|
+
check_view_options=check_in.view_options,
|
|
63
67
|
)
|
|
64
68
|
else:
|
|
65
69
|
check = create_check_without_run(
|
|
@@ -67,12 +71,15 @@ async def create_check(check_in: CreateCheckIn, background_tasks: BackgroundTask
|
|
|
67
71
|
check_description=check_in.description,
|
|
68
72
|
check_type=check_in.type,
|
|
69
73
|
params=check_in.params,
|
|
70
|
-
check_view_options=check_in.view_options
|
|
74
|
+
check_view_options=check_in.view_options,
|
|
71
75
|
)
|
|
72
|
-
log_api_event(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
log_api_event(
|
|
77
|
+
"create_check",
|
|
78
|
+
dict(
|
|
79
|
+
type=str(check.type),
|
|
80
|
+
track_props=check_in.track_props,
|
|
81
|
+
),
|
|
82
|
+
)
|
|
76
83
|
except NameError as e:
|
|
77
84
|
raise HTTPException(status_code=404, detail=str(e))
|
|
78
85
|
except ValueError as e:
|
|
@@ -93,10 +100,13 @@ async def run_check_handler(check_id: UUID, input: RunCheckIn):
|
|
|
93
100
|
raise HTTPException(status_code=404, detail=f"Check ID '{check_id}' not found")
|
|
94
101
|
|
|
95
102
|
try:
|
|
96
|
-
log_api_event(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
log_api_event(
|
|
104
|
+
"rerun_check",
|
|
105
|
+
dict(
|
|
106
|
+
type=str(check.type),
|
|
107
|
+
rerun=True,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
100
110
|
run, future = submit_run(check.type, check.params, check_id=check_id)
|
|
101
111
|
except RecceException as e:
|
|
102
112
|
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -122,7 +132,7 @@ async def list_checks_handler():
|
|
|
122
132
|
async def get_check_handler(check_id: UUID):
|
|
123
133
|
check = CheckDAO().find_check_by_id(check_id)
|
|
124
134
|
if check is None:
|
|
125
|
-
raise HTTPException(status_code=404, detail=
|
|
135
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
126
136
|
|
|
127
137
|
runs = RunDAO().list_by_check_id(check_id)
|
|
128
138
|
last_run = runs[-1] if len(runs) > 0 else None
|
|
@@ -142,21 +152,9 @@ class PatchCheckIn(BaseModel):
|
|
|
142
152
|
|
|
143
153
|
@check_router.patch("/checks/{check_id}", status_code=200, response_model=CheckOut, response_model_exclude_none=True)
|
|
144
154
|
async def update_check_handler(check_id: UUID, patch: PatchCheckIn, background_tasks: BackgroundTasks):
|
|
145
|
-
check = CheckDAO().
|
|
155
|
+
check = CheckDAO().update_check_by_id(check_id, patch)
|
|
146
156
|
if check is None:
|
|
147
|
-
raise HTTPException(status_code=404, detail=
|
|
148
|
-
|
|
149
|
-
if patch.name is not None:
|
|
150
|
-
check.name = patch.name
|
|
151
|
-
if patch.description is not None:
|
|
152
|
-
check.description = patch.description
|
|
153
|
-
if patch.params is not None:
|
|
154
|
-
check.params = patch.params
|
|
155
|
-
if patch.view_options is not None:
|
|
156
|
-
check.view_options = patch.view_options
|
|
157
|
-
if patch.is_checked is not None:
|
|
158
|
-
check.is_checked = patch.is_checked
|
|
159
|
-
check.updated_at = datetime.now(timezone.utc).replace(microsecond=0)
|
|
157
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
160
158
|
|
|
161
159
|
background_tasks.add_task(export_persistent_state)
|
|
162
160
|
return CheckOut.from_check(check)
|
|
@@ -184,3 +182,22 @@ async def reorder_handler(order: ReorderChecksIn):
|
|
|
184
182
|
CheckDAO().reorder(order.source, order.destination)
|
|
185
183
|
except RecceException as e:
|
|
186
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))
|