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/run.py
CHANGED
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
import sys
|
|
3
3
|
import time
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
|
-
from typing import List
|
|
5
|
+
from typing import Dict, List, Tuple
|
|
6
6
|
|
|
7
7
|
from deepdiff import DeepDiff
|
|
8
8
|
from rich import box
|
|
@@ -17,6 +17,7 @@ from recce.apis.check_func import (
|
|
|
17
17
|
from recce.apis.run_func import submit_run
|
|
18
18
|
from recce.config import RecceConfig
|
|
19
19
|
from recce.core import default_context
|
|
20
|
+
from recce.models import CheckDAO
|
|
20
21
|
from recce.models.types import RunType
|
|
21
22
|
from recce.summary import generate_markdown_summary
|
|
22
23
|
|
|
@@ -111,7 +112,7 @@ def run_should_be_approved(run):
|
|
|
111
112
|
return False
|
|
112
113
|
|
|
113
114
|
|
|
114
|
-
async def execute_preset_checks(preset_checks:
|
|
115
|
+
async def execute_preset_checks(preset_checks: List, is_skip_query: bool) -> Tuple[int, List[Dict]]:
|
|
115
116
|
"""
|
|
116
117
|
Execute the preset checks
|
|
117
118
|
"""
|
|
@@ -155,12 +156,18 @@ async def execute_preset_checks(preset_checks: list) -> (int, List[dict]):
|
|
|
155
156
|
is_checked=is_check,
|
|
156
157
|
)
|
|
157
158
|
else:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
if not is_skip_query:
|
|
160
|
+
run, future = submit_run(check_type, params=check_params)
|
|
161
|
+
await future
|
|
162
|
+
is_check = run_should_be_approved(run)
|
|
163
|
+
create_check_from_run(
|
|
164
|
+
run.run_id, check_name, check_description, check_options, is_preset=True, is_checked=is_check
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
create_check_without_run(
|
|
168
|
+
check_name, check_description, check_type, check_params, check_options, is_preset=True
|
|
169
|
+
)
|
|
170
|
+
continue
|
|
164
171
|
|
|
165
172
|
end = time.time()
|
|
166
173
|
table.add_row(
|
|
@@ -200,7 +207,7 @@ async def execute_preset_checks(preset_checks: list) -> (int, List[dict]):
|
|
|
200
207
|
return rc, failed_checks
|
|
201
208
|
|
|
202
209
|
|
|
203
|
-
async def execute_state_checks(checks:
|
|
210
|
+
async def execute_state_checks(checks: List, is_skip_query: bool) -> Tuple[int, List[Dict]]:
|
|
204
211
|
"""
|
|
205
212
|
Execute the checks from loaded state
|
|
206
213
|
"""
|
|
@@ -232,7 +239,7 @@ async def execute_state_checks(checks: list) -> (int, List[dict]):
|
|
|
232
239
|
raise ValueError(f"Invalid check type: {check_type}")
|
|
233
240
|
|
|
234
241
|
start = time.time()
|
|
235
|
-
if check_type not in ["schema_diff"]:
|
|
242
|
+
if check_type not in ["schema_diff", "lineage_diff"] and not is_skip_query:
|
|
236
243
|
run, future = submit_run(check_type, params=check_params, check_id=check_id)
|
|
237
244
|
await future
|
|
238
245
|
|
|
@@ -295,7 +302,7 @@ def process_failed_checks(failed_checks: List[dict], error_log=None):
|
|
|
295
302
|
content += markdown_table(failed_check_table).set_params(quote=False, row_sep="markdown").get_markdown()
|
|
296
303
|
|
|
297
304
|
if error_log:
|
|
298
|
-
with open(error_log, "w") as f:
|
|
305
|
+
with open(error_log, "w", encoding="utf-8") as f:
|
|
299
306
|
f.write(content)
|
|
300
307
|
print(f"The failed checks are stored at '{error_log}'")
|
|
301
308
|
else:
|
|
@@ -314,7 +321,17 @@ async def cli_run(output_state_file: str, **kwargs):
|
|
|
314
321
|
|
|
315
322
|
ctx = load_context(**kwargs)
|
|
316
323
|
|
|
324
|
+
# Set up the checks if this is a session-based run
|
|
325
|
+
if kwargs.get("session_id") and kwargs.get("state_loader"):
|
|
326
|
+
state_loader = kwargs.get("state_loader")
|
|
327
|
+
try:
|
|
328
|
+
# Try to populate the checks from the database
|
|
329
|
+
state_loader.state.checks = CheckDAO().list()
|
|
330
|
+
except Exception as e:
|
|
331
|
+
console.print(f"[[red]Error[/red]] Failed to load checks from database: {e}")
|
|
332
|
+
|
|
317
333
|
is_skip_query = kwargs.get("skip_query", False)
|
|
334
|
+
is_skip_check = kwargs.get("skip_check", False)
|
|
318
335
|
|
|
319
336
|
# Prepare the artifact by collecting the lineage
|
|
320
337
|
console.rule("DBT Artifacts")
|
|
@@ -327,23 +344,23 @@ async def cli_run(output_state_file: str, **kwargs):
|
|
|
327
344
|
rc = 0
|
|
328
345
|
if ctx.state_loader.state is None:
|
|
329
346
|
preset_checks = RecceConfig().get("checks")
|
|
330
|
-
if
|
|
347
|
+
if is_skip_check or preset_checks is None or len(preset_checks) == 0:
|
|
331
348
|
# Skip the preset checks
|
|
332
349
|
pass
|
|
333
350
|
else:
|
|
334
351
|
console.rule("Preset checks")
|
|
335
|
-
_, failed_checks = await execute_preset_checks(preset_checks)
|
|
352
|
+
_, failed_checks = await execute_preset_checks(preset_checks, is_skip_query)
|
|
336
353
|
if failed_checks:
|
|
337
354
|
console.print("[[yellow]Warning[/yellow]] Preset checks failed. Please see the failed reason.")
|
|
338
355
|
process_failed_checks(failed_checks, error_log)
|
|
339
356
|
else:
|
|
340
357
|
state_checks = ctx.state_loader.state.checks
|
|
341
|
-
if
|
|
358
|
+
if is_skip_check or state_checks is None or len(state_checks) == 0:
|
|
342
359
|
# Skip the checks in the state
|
|
343
360
|
pass
|
|
344
361
|
else:
|
|
345
362
|
console.rule("Checks")
|
|
346
|
-
_, failed_checks = await execute_state_checks(state_checks)
|
|
363
|
+
_, failed_checks = await execute_state_checks(state_checks, is_skip_query)
|
|
347
364
|
if failed_checks:
|
|
348
365
|
console.print("[[yellow]Warning[/yellow]] Checks failed. Please see the failed reason.")
|
|
349
366
|
process_failed_checks(failed_checks, error_log)
|
|
@@ -356,14 +373,17 @@ async def cli_run(output_state_file: str, **kwargs):
|
|
|
356
373
|
console.rule("Export state")
|
|
357
374
|
ctx.state_loader.state_file = output_state_file
|
|
358
375
|
msg = ctx.state_loader.export(ctx.export_state())
|
|
359
|
-
|
|
376
|
+
if msg is not None:
|
|
377
|
+
console.print(msg)
|
|
378
|
+
else:
|
|
379
|
+
console.print("Export successful")
|
|
360
380
|
|
|
361
381
|
summary_path = kwargs.get("summary")
|
|
362
382
|
if summary_path:
|
|
363
383
|
dirs = os.path.dirname(summary_path)
|
|
364
384
|
if dirs:
|
|
365
385
|
os.makedirs(dirs, exist_ok=True)
|
|
366
|
-
with open(summary_path, "w") as f:
|
|
386
|
+
with open(summary_path, "w", encoding="utf-8") as f:
|
|
367
387
|
f.write(generate_markdown_summary(ctx))
|
|
368
388
|
console.print(f"The summary is stored at '{summary_path}'")
|
|
369
389
|
|
recce/server.py
CHANGED
|
@@ -7,6 +7,7 @@ import uuid
|
|
|
7
7
|
from contextlib import asynccontextmanager
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from datetime import datetime, timedelta
|
|
10
|
+
from enum import Enum
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Annotated, Any, Literal, Optional, Set
|
|
12
13
|
|
|
@@ -29,8 +30,9 @@ from starlette.middleware.gzip import GZipMiddleware
|
|
|
29
30
|
from starlette.middleware.sessions import SessionMiddleware
|
|
30
31
|
from starlette.websockets import WebSocketDisconnect
|
|
31
32
|
|
|
32
|
-
from . import __latest_version__, __version__, event
|
|
33
|
+
from . import __latest_version__, __version__, event, is_recce_cloud_instance
|
|
33
34
|
from .apis.check_api import check_router
|
|
35
|
+
from .apis.check_events_api import check_events_router
|
|
34
36
|
from .apis.run_api import run_router
|
|
35
37
|
from .config import RecceConfig
|
|
36
38
|
from .connect_to_cloud import (
|
|
@@ -43,12 +45,31 @@ from .connect_to_cloud import (
|
|
|
43
45
|
from .core import RecceContext, default_context, load_context
|
|
44
46
|
from .event import get_recce_api_token, log_api_event, log_single_env_event
|
|
45
47
|
from .exceptions import RecceException
|
|
48
|
+
from .github import is_github_codespace
|
|
46
49
|
from .models.types import CllData
|
|
47
50
|
from .run import load_preset_checks
|
|
48
51
|
from .state import RecceShareStateManager, RecceStateLoader
|
|
52
|
+
from .util.startup_perf import track_timing
|
|
49
53
|
|
|
50
54
|
logger = logging.getLogger("uvicorn")
|
|
51
55
|
|
|
56
|
+
# Idle timeout check interval bounds (in seconds)
|
|
57
|
+
MAX_CHECK_INTERVAL = 30
|
|
58
|
+
MIN_CHECK_INTERVAL = 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class RecceServerMode(str, Enum):
|
|
62
|
+
server = "server"
|
|
63
|
+
preview = "preview"
|
|
64
|
+
read_only = "read-only"
|
|
65
|
+
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return self.value
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def available_members() -> Set[str]:
|
|
71
|
+
return ["server", "preview", "read-only"]
|
|
72
|
+
|
|
52
73
|
|
|
53
74
|
@dataclass
|
|
54
75
|
class AppState:
|
|
@@ -59,13 +80,19 @@ class AppState:
|
|
|
59
80
|
auth_options: Optional[dict] = None
|
|
60
81
|
lifetime: Optional[int] = None
|
|
61
82
|
lifetime_expired_at: Optional[datetime] = None
|
|
83
|
+
idle_timeout: Optional[int] = None
|
|
84
|
+
last_activity: Optional[dict] = None
|
|
62
85
|
share_url: Optional[str] = None
|
|
86
|
+
organization_name: Optional[str] = None
|
|
87
|
+
web_url: Optional[str] = None
|
|
88
|
+
host: Optional[str] = None
|
|
89
|
+
port: Optional[int] = None
|
|
63
90
|
|
|
64
91
|
|
|
65
92
|
def schedule_lifetime_termination(app_state):
|
|
66
93
|
def terminating_server():
|
|
67
94
|
pid = os.getpid()
|
|
68
|
-
logger.info(f"Terminating server process [{pid}] manually")
|
|
95
|
+
logger.info(f"Terminating server process [{pid}] manually due to lifetime expiration")
|
|
69
96
|
os.kill(pid, signal.SIGINT)
|
|
70
97
|
|
|
71
98
|
# Terminate the server process after the specified lifetime
|
|
@@ -74,6 +101,56 @@ def schedule_lifetime_termination(app_state):
|
|
|
74
101
|
asyncio.get_running_loop().call_later(app_state.lifetime, terminating_server)
|
|
75
102
|
|
|
76
103
|
|
|
104
|
+
def schedule_idle_timeout_check(app_state):
|
|
105
|
+
"""
|
|
106
|
+
Schedule periodic checks for idle timeout.
|
|
107
|
+
If the server has been idle for longer than idle_timeout, terminate it.
|
|
108
|
+
"""
|
|
109
|
+
# Track last activity time in app_state
|
|
110
|
+
app_state.last_activity = {"time": datetime.now(utc)}
|
|
111
|
+
|
|
112
|
+
def terminating_server_idle():
|
|
113
|
+
pid = os.getpid()
|
|
114
|
+
logger.info(f"Terminating server process [{pid}] manually due to idle timeout")
|
|
115
|
+
os.kill(pid, signal.SIGINT)
|
|
116
|
+
|
|
117
|
+
async def check_idle_timeout():
|
|
118
|
+
"""Periodically check if the server has been idle for too long"""
|
|
119
|
+
# Use smaller check interval if idle_timeout is very short
|
|
120
|
+
# Check at least every MAX_CHECK_INTERVAL seconds, but also check when idle_timeout is approaching
|
|
121
|
+
check_interval = min(MAX_CHECK_INTERVAL, max(MIN_CHECK_INTERVAL, app_state.idle_timeout // 3))
|
|
122
|
+
|
|
123
|
+
logger.debug(f"[Idle Timeout] Starting idle timeout checker with {check_interval}s check interval")
|
|
124
|
+
|
|
125
|
+
while True:
|
|
126
|
+
await asyncio.sleep(check_interval)
|
|
127
|
+
|
|
128
|
+
idle_seconds = (datetime.now(utc) - app_state.last_activity["time"]).total_seconds()
|
|
129
|
+
remaining_seconds = app_state.idle_timeout - idle_seconds
|
|
130
|
+
|
|
131
|
+
# Always log the countdown for debugging
|
|
132
|
+
if remaining_seconds > 0:
|
|
133
|
+
logger.debug(
|
|
134
|
+
f"[Idle Timeout] Server idle for {idle_seconds:.1f}s / {app_state.idle_timeout}s "
|
|
135
|
+
f"(remaining: {remaining_seconds:.1f}s)"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if idle_seconds >= app_state.idle_timeout:
|
|
139
|
+
logger.info(
|
|
140
|
+
f"[Idle Timeout] Threshold reached! Server has been idle for {idle_seconds:.0f} seconds "
|
|
141
|
+
f"(threshold: {app_state.idle_timeout} seconds)"
|
|
142
|
+
)
|
|
143
|
+
terminating_server_idle()
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
# Start the idle timeout check task
|
|
147
|
+
logger.info(f"[Configuration] The idle timeout of the server is {app_state.idle_timeout} seconds")
|
|
148
|
+
|
|
149
|
+
# Create task using asyncio.create_task which works in async context
|
|
150
|
+
task = asyncio.create_task(check_idle_timeout())
|
|
151
|
+
logger.debug(f"[Idle Timeout] Background task created: {task}")
|
|
152
|
+
|
|
153
|
+
|
|
77
154
|
def setup_server(app_state: AppState) -> RecceContext:
|
|
78
155
|
from rich.console import Console
|
|
79
156
|
|
|
@@ -103,46 +180,99 @@ def setup_server(app_state: AppState) -> RecceContext:
|
|
|
103
180
|
|
|
104
181
|
log_load_state(command="server", single_env=single_env)
|
|
105
182
|
|
|
106
|
-
if app_state.lifetime is not None and app_state.lifetime > 0:
|
|
107
|
-
schedule_lifetime_termination(app_state)
|
|
108
|
-
|
|
109
183
|
return ctx
|
|
110
184
|
|
|
111
185
|
|
|
112
186
|
def teardown_server(app_state: AppState, ctx: RecceContext):
|
|
113
|
-
|
|
187
|
+
# pull latest state, merge runs/checks and pick the newer artifacts
|
|
188
|
+
state_loader = ctx.state_loader
|
|
189
|
+
state_loader.refresh()
|
|
190
|
+
if state_loader.state:
|
|
191
|
+
ctx.import_state(state_loader.state, merge=True)
|
|
114
192
|
state_loader.export(ctx.export_state())
|
|
115
|
-
|
|
116
193
|
ctx.stop_monitor_artifacts()
|
|
117
194
|
if app_state.flag.get("single_env_onboarding", False):
|
|
118
195
|
ctx.stop_monitor_base_env()
|
|
119
196
|
|
|
120
197
|
|
|
121
198
|
def setup_ready_only(app_state: AppState):
|
|
122
|
-
|
|
123
|
-
schedule_lifetime_termination(app_state)
|
|
199
|
+
pass
|
|
124
200
|
|
|
125
201
|
|
|
126
202
|
def teardown_ready_only(app_state: AppState):
|
|
127
203
|
pass
|
|
128
204
|
|
|
129
205
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
206
|
+
def setup_preview(app_state: AppState):
|
|
207
|
+
state_loader = app_state.state_loader
|
|
208
|
+
kwargs = app_state.kwargs
|
|
209
|
+
ctx = load_context(**kwargs, state_loader=state_loader)
|
|
210
|
+
return ctx
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def teardown_preview(app_state: AppState, ctx: RecceContext):
|
|
214
|
+
state_loader = app_state.state_loader
|
|
215
|
+
state_loader.export(ctx.export_state())
|
|
216
|
+
pass
|
|
134
217
|
|
|
218
|
+
|
|
219
|
+
@track_timing("server_setup")
|
|
220
|
+
def _do_lifespan_setup(app_state: AppState):
|
|
221
|
+
"""Run server setup and return context for teardown."""
|
|
135
222
|
if app_state.command == "server":
|
|
136
223
|
ctx = setup_server(app_state)
|
|
137
|
-
elif app_state.command == "
|
|
224
|
+
elif app_state.command == "read-only":
|
|
138
225
|
setup_ready_only(app_state)
|
|
226
|
+
ctx = None
|
|
227
|
+
elif app_state.command == "preview":
|
|
228
|
+
ctx = setup_preview(app_state)
|
|
229
|
+
else:
|
|
230
|
+
ctx = None
|
|
231
|
+
|
|
232
|
+
if app_state.lifetime is not None and app_state.lifetime > 0:
|
|
233
|
+
schedule_lifetime_termination(app_state)
|
|
234
|
+
|
|
235
|
+
if app_state.idle_timeout is not None and app_state.idle_timeout > 0:
|
|
236
|
+
logger.debug(f"[Idle Timeout] Scheduling idle timeout check with {app_state.idle_timeout} seconds")
|
|
237
|
+
schedule_idle_timeout_check(app_state)
|
|
238
|
+
|
|
239
|
+
return ctx
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@asynccontextmanager
|
|
243
|
+
async def lifespan(fastapi: FastAPI):
|
|
244
|
+
from recce.core import default_context
|
|
245
|
+
from recce.event import log_performance
|
|
246
|
+
from recce.util.startup_perf import clear_startup_tracker, get_startup_tracker
|
|
247
|
+
|
|
248
|
+
app_state: AppState = app.state
|
|
249
|
+
|
|
250
|
+
# Ensure logger is at DEBUG level if debug mode is enabled
|
|
251
|
+
if app_state.kwargs and app_state.kwargs.get("debug"):
|
|
252
|
+
logger.setLevel(logging.DEBUG)
|
|
253
|
+
logger.debug("Debug mode enabled - logger set to DEBUG level")
|
|
254
|
+
|
|
255
|
+
ctx = _do_lifespan_setup(app_state)
|
|
256
|
+
|
|
257
|
+
# Log startup performance metrics
|
|
258
|
+
if tracker := get_startup_tracker():
|
|
259
|
+
tracker.command = app_state.command
|
|
260
|
+
recce_ctx = default_context()
|
|
261
|
+
if recce_ctx and recce_ctx.adapter:
|
|
262
|
+
tracker.adapter_type = type(recce_ctx.adapter).__name__
|
|
263
|
+
if hasattr(recce_ctx.adapter, "curr_manifest") and recce_ctx.adapter.curr_manifest:
|
|
264
|
+
tracker.node_count = len(recce_ctx.adapter.curr_manifest.nodes)
|
|
265
|
+
log_performance("server_startup", tracker.to_dict())
|
|
266
|
+
clear_startup_tracker()
|
|
139
267
|
|
|
140
268
|
yield
|
|
141
269
|
|
|
142
270
|
if app_state.command == "server":
|
|
143
271
|
teardown_server(app_state, ctx)
|
|
144
|
-
elif app_state.command == "
|
|
272
|
+
elif app_state.command == "read-only":
|
|
145
273
|
teardown_ready_only(app_state)
|
|
274
|
+
elif app_state.command == "preview":
|
|
275
|
+
teardown_preview(app_state, ctx)
|
|
146
276
|
|
|
147
277
|
|
|
148
278
|
app = FastAPI(lifespan=lifespan)
|
|
@@ -150,7 +280,7 @@ app = FastAPI(lifespan=lifespan)
|
|
|
150
280
|
|
|
151
281
|
def verify_json_file(file_path: str) -> bool:
|
|
152
282
|
try:
|
|
153
|
-
with open(file_path, "r") as f:
|
|
283
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
154
284
|
json.load(f)
|
|
155
285
|
except Exception:
|
|
156
286
|
return False
|
|
@@ -207,6 +337,27 @@ app.add_middleware(
|
|
|
207
337
|
)
|
|
208
338
|
|
|
209
339
|
|
|
340
|
+
@app.middleware("http")
|
|
341
|
+
async def track_activity_for_idle_timeout(request: Request, call_next):
|
|
342
|
+
"""Track activity time for idle timeout check"""
|
|
343
|
+
# Exclude paths that should not reset idle timer
|
|
344
|
+
# Health checks and monitoring endpoints don't count as user activity
|
|
345
|
+
excluded_paths = ["/api/health", "/api/ws"]
|
|
346
|
+
|
|
347
|
+
# Update last activity time BEFORE processing request if idle timeout is enabled
|
|
348
|
+
# This ensures long-running requests don't get terminated mid-execution
|
|
349
|
+
app_state: AppState = app.state
|
|
350
|
+
if app_state.last_activity is not None:
|
|
351
|
+
if request.url.path not in excluded_paths:
|
|
352
|
+
app_state.last_activity["time"] = datetime.now(utc)
|
|
353
|
+
logger.debug(f"[Idle Timeout] ✓ Activity detected: {request.method} {request.url.path} - Timer reset")
|
|
354
|
+
else:
|
|
355
|
+
logger.debug(f"[Idle Timeout] Excluded path (no timer reset): {request.method} {request.url.path}")
|
|
356
|
+
|
|
357
|
+
response = await call_next(request)
|
|
358
|
+
return response
|
|
359
|
+
|
|
360
|
+
|
|
210
361
|
@app.middleware("http")
|
|
211
362
|
async def set_context_by_cookie(request: Request, call_next):
|
|
212
363
|
response = await call_next(request)
|
|
@@ -239,12 +390,30 @@ async def health_check(request: Request):
|
|
|
239
390
|
return {"status": "ok"}
|
|
240
391
|
|
|
241
392
|
|
|
393
|
+
@app.post("/api/keep-alive")
|
|
394
|
+
async def keep_alive():
|
|
395
|
+
"""Endpoint to keep the session alive and reset idle timeout"""
|
|
396
|
+
app_state: AppState = app.state
|
|
397
|
+
if app_state.last_activity is not None:
|
|
398
|
+
app_state.last_activity["time"] = datetime.now(utc)
|
|
399
|
+
logger.debug("[Idle Timeout] Keep-alive request received - Timer reset")
|
|
400
|
+
return {"status": "ok", "idle_timeout_enabled": True}
|
|
401
|
+
return {"status": "ok", "idle_timeout_enabled": False}
|
|
402
|
+
|
|
403
|
+
|
|
242
404
|
class RecceInstanceInfoOut(BaseModel):
|
|
405
|
+
server_mode: RecceServerMode
|
|
243
406
|
read_only: bool
|
|
407
|
+
preview: bool
|
|
244
408
|
single_env: bool
|
|
245
409
|
authed: bool
|
|
410
|
+
cloud_instance: bool
|
|
246
411
|
lifetime_expired_at: Optional[datetime] = None
|
|
412
|
+
idle_timeout: Optional[int] = None
|
|
247
413
|
share_url: Optional[str] = None
|
|
414
|
+
session_id: Optional[str] = None
|
|
415
|
+
organization_name: Optional[str] = None
|
|
416
|
+
web_url: Optional[str] = None
|
|
248
417
|
|
|
249
418
|
|
|
250
419
|
@app.get("/api/instance-info", response_model=RecceInstanceInfoOut, response_model_exclude_none=True)
|
|
@@ -257,11 +426,18 @@ async def recce_instance_info():
|
|
|
257
426
|
api_token = get_recce_api_token()
|
|
258
427
|
|
|
259
428
|
return {
|
|
429
|
+
"server_mode": app_state.command,
|
|
260
430
|
"read_only": read_only,
|
|
431
|
+
"preview": flag.get("preview", False),
|
|
261
432
|
"single_env": single_env,
|
|
262
433
|
"authed": True if api_token else False,
|
|
434
|
+
"cloud_instance": is_recce_cloud_instance(),
|
|
263
435
|
"lifetime_expired_at": app_state.lifetime_expired_at, # UTC timezone
|
|
436
|
+
"idle_timeout": app_state.idle_timeout,
|
|
264
437
|
"share_url": app_state.share_url,
|
|
438
|
+
"session_id": app_state.state_loader.session_id if app_state.state_loader else None,
|
|
439
|
+
"organization_name": app_state.organization_name,
|
|
440
|
+
"web_url": app_state.web_url,
|
|
265
441
|
# TODO: Add more instance info which won't change during the instance lifecycle
|
|
266
442
|
# review_mode
|
|
267
443
|
# cloud_mode
|
|
@@ -289,6 +465,7 @@ async def get_info():
|
|
|
289
465
|
"""
|
|
290
466
|
context = default_context()
|
|
291
467
|
demo = os.environ.get("DEMO", False)
|
|
468
|
+
is_codespace = is_github_codespace()
|
|
292
469
|
|
|
293
470
|
if demo:
|
|
294
471
|
state = context.export_demo_state()
|
|
@@ -313,6 +490,7 @@ async def get_info():
|
|
|
313
490
|
"pull_request": state.pull_request.to_dict() if state.pull_request else None,
|
|
314
491
|
"lineage": lineage_diff,
|
|
315
492
|
"demo": bool(demo),
|
|
493
|
+
"codespace": bool(is_codespace),
|
|
316
494
|
"cloud_mode": context.state_loader.cloud_mode,
|
|
317
495
|
"file_mode": context.state_loader.state_file is not None,
|
|
318
496
|
"filename": filename,
|
|
@@ -337,9 +515,9 @@ class CllIn(BaseModel):
|
|
|
337
515
|
node_id: Optional[str] = None
|
|
338
516
|
column: Optional[str] = None
|
|
339
517
|
change_analysis: Optional[bool] = False
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
518
|
+
no_cll: Optional[bool] = False
|
|
519
|
+
no_upstream: Optional[bool] = False
|
|
520
|
+
no_downstream: Optional[bool] = False
|
|
343
521
|
|
|
344
522
|
|
|
345
523
|
class CllOutput(BaseModel):
|
|
@@ -355,8 +533,9 @@ async def column_level_lineage_by_node(cll_input: CllIn):
|
|
|
355
533
|
node_id=cll_input.node_id,
|
|
356
534
|
column=cll_input.column,
|
|
357
535
|
change_analysis=cll_input.change_analysis,
|
|
358
|
-
|
|
359
|
-
|
|
536
|
+
no_upstream=cll_input.no_upstream,
|
|
537
|
+
no_downstream=cll_input.no_downstream,
|
|
538
|
+
no_cll=cll_input.no_cll,
|
|
360
539
|
)
|
|
361
540
|
|
|
362
541
|
return CllOutput(current=cll)
|
|
@@ -674,9 +853,25 @@ async def generate_connect_to_cloud_url(background_tasks: BackgroundTasks):
|
|
|
674
853
|
}
|
|
675
854
|
|
|
676
855
|
|
|
856
|
+
@app.get("/api/users")
|
|
857
|
+
async def get_user_info():
|
|
858
|
+
from recce.connect_to_cloud import RecceCloud
|
|
859
|
+
|
|
860
|
+
context = default_context()
|
|
861
|
+
user_token = get_recce_api_token() or context.state_loader.token
|
|
862
|
+
cloud = RecceCloud(user_token)
|
|
863
|
+
try:
|
|
864
|
+
user_info = cloud.get_user_info()
|
|
865
|
+
return user_info
|
|
866
|
+
except Exception as e:
|
|
867
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
868
|
+
|
|
869
|
+
|
|
677
870
|
api_prefix = "/api"
|
|
678
871
|
app.include_router(check_router, prefix=api_prefix)
|
|
872
|
+
app.include_router(check_events_router, prefix=api_prefix)
|
|
679
873
|
app.include_router(run_router, prefix=api_prefix)
|
|
680
874
|
|
|
681
875
|
static_folder_path = Path(__file__).parent / "data"
|
|
876
|
+
|
|
682
877
|
app.mount("/", StaticFiles(directory=static_folder_path, html=True), name="static")
|
recce/state/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .cloud import (
|
|
2
|
+
CloudStateLoader,
|
|
3
|
+
RecceCloudStateManager,
|
|
4
|
+
RecceShareStateManager,
|
|
5
|
+
s3_sse_c_headers,
|
|
6
|
+
)
|
|
7
|
+
from .const import ErrorMessage
|
|
8
|
+
from .local import FileStateLoader
|
|
9
|
+
from .state import (
|
|
10
|
+
ArtifactsRoot,
|
|
11
|
+
GitRepoInfo,
|
|
12
|
+
PullRequestInfo,
|
|
13
|
+
RecceState,
|
|
14
|
+
RecceStateMetadata,
|
|
15
|
+
)
|
|
16
|
+
from .state_loader import RecceStateLoader
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ArtifactsRoot",
|
|
20
|
+
"ErrorMessage",
|
|
21
|
+
"RecceCloudStateManager",
|
|
22
|
+
"RecceShareStateManager",
|
|
23
|
+
"RecceState",
|
|
24
|
+
"RecceStateLoader",
|
|
25
|
+
"CloudStateLoader",
|
|
26
|
+
"FileStateLoader",
|
|
27
|
+
"RecceStateMetadata",
|
|
28
|
+
"s3_sse_c_headers",
|
|
29
|
+
"GitRepoInfo",
|
|
30
|
+
"PullRequestInfo",
|
|
31
|
+
]
|