recce-nightly 1.9.0.20250623__py3-none-any.whl → 1.25.0.20251112a2066__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.
- recce/VERSION +1 -1
- recce/__init__.py +5 -0
- recce/adapter/dbt_adapter/__init__.py +318 -240
- recce/artifact.py +76 -3
- recce/cli.py +703 -71
- recce/config.py +3 -3
- recce/connect_to_cloud.py +138 -0
- recce/core.py +3 -3
- recce/data/404.html +1 -22
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +12 -0
- recce/data/_next/static/6LypcDXgyuSaiSCrsmUub/_buildManifest.js +11 -0
- recce/data/_next/static/6LypcDXgyuSaiSCrsmUub/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/0a2b2dd4b57049c2.js +1 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/24fd885c7180a612.js +1 -0
- recce/data/_next/static/chunks/27e66b2eab4adc32.js +19 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/917619ab62a32388.js +1 -0
- recce/data/_next/static/chunks/93ba5a62932b704f.js +4 -0
- recce/data/_next/static/chunks/a43a2a5e06d5a92b.js +1 -0
- recce/data/_next/static/chunks/a6c78b24bd8b84fc.js +1 -0
- recce/data/_next/static/chunks/b2610ba997ff8c4f.js +110 -0
- recce/data/_next/static/chunks/ba2d87265a68599d.css +2 -0
- recce/data/_next/static/chunks/c117fd1c1382dd83.js +11 -0
- recce/data/_next/static/chunks/c9425ca46eebdde9.js +1 -0
- recce/data/_next/static/chunks/cc8a9eadba012be0.css +6 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/e392ad92847c3e17.js +1 -0
- recce/data/_next/static/chunks/e4ce95efe88dae79.js +11 -0
- recce/data/_next/static/chunks/e69c777814fea6ed.js +2 -0
- recce/data/_next/static/chunks/turbopack-21cfd73037ff57ab.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/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._index.txt +8 -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 +10 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +68 -0
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -8
- 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 +632 -0
- recce/models/types.py +23 -2
- recce/pull_request.py +1 -1
- recce/run.py +23 -16
- recce/server.py +194 -19
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +632 -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 +2 -1
- recce/tasks/dataframe.py +59 -2
- recce/tasks/rowcount.py +4 -1
- recce/tasks/schema.py +4 -1
- recce/tasks/valuediff.py +1 -1
- recce/util/api_token.py +11 -2
- recce/util/breaking.py +9 -0
- recce/util/cll.py +1 -2
- recce/util/io.py +2 -2
- recce/util/lineage.py +19 -18
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +229 -5
- recce/yaml/__init__.py +2 -2
- recce_cloud/__init__.py +15 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +104 -0
- recce_cloud/api/client.py +150 -0
- recce_cloud/api/exceptions.py +26 -0
- recce_cloud/api/factory.py +63 -0
- recce_cloud/api/github.py +72 -0
- recce_cloud/api/gitlab.py +78 -0
- recce_cloud/artifact.py +57 -0
- recce_cloud/ci_providers/__init__.py +9 -0
- recce_cloud/ci_providers/base.py +82 -0
- recce_cloud/ci_providers/detector.py +147 -0
- recce_cloud/ci_providers/github_actions.py +136 -0
- recce_cloud/ci_providers/gitlab_ci.py +130 -0
- recce_cloud/cli.py +303 -0
- recce_cloud/upload.py +213 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/METADATA +31 -27
- recce_nightly-1.25.0.20251112a2066.dist-info/RECORD +178 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/test_dbt_cll.py +412 -79
- tests/recce_cloud/__init__.py +0 -0
- tests/recce_cloud/test_ci_providers.py +351 -0
- tests/recce_cloud/test_cli.py +372 -0
- tests/recce_cloud/test_client.py +273 -0
- tests/recce_cloud/test_platform_clients.py +279 -0
- tests/test_cli.py +106 -3
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_connect_to_cloud.py +82 -0
- tests/test_core.py +148 -3
- tests/test_mcp_server.py +332 -0
- tests/test_server.py +6 -6
- tests/test_summary.py +14 -6
- recce/data/_next/static/WrRUb3nV8BhAZG_R8kVma/_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-7ab55ae02606193c.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-59241c42b7dd4fcf.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/state.py +0 -785
- recce_nightly-1.9.0.20250623.dist-info/RECORD +0 -151
- tests/test_state.py +0 -134
- /recce/data/_next/static/{WrRUb3nV8BhAZG_R8kVma → 6LypcDXgyuSaiSCrsmUub}/_ssgManifest.js +0 -0
- /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/media/{reload-image.79aabb7d.svg → reload-image.7aa931c7.svg} +0 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/WHEEL +0 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.9.0.20250623.dist-info → recce_nightly-1.25.0.20251112a2066.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
|
|
@@ -111,7 +111,7 @@ def run_should_be_approved(run):
|
|
|
111
111
|
return False
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
async def execute_preset_checks(preset_checks:
|
|
114
|
+
async def execute_preset_checks(preset_checks: List, is_skip_query: bool) -> Tuple[int, List[Dict]]:
|
|
115
115
|
"""
|
|
116
116
|
Execute the preset checks
|
|
117
117
|
"""
|
|
@@ -155,12 +155,18 @@ async def execute_preset_checks(preset_checks: list) -> (int, List[dict]):
|
|
|
155
155
|
is_checked=is_check,
|
|
156
156
|
)
|
|
157
157
|
else:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
if not is_skip_query:
|
|
159
|
+
run, future = submit_run(check_type, params=check_params)
|
|
160
|
+
await future
|
|
161
|
+
is_check = run_should_be_approved(run)
|
|
162
|
+
create_check_from_run(
|
|
163
|
+
run.run_id, check_name, check_description, check_options, is_preset=True, is_checked=is_check
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
create_check_without_run(
|
|
167
|
+
check_name, check_description, check_type, check_params, check_options, is_preset=True
|
|
168
|
+
)
|
|
169
|
+
continue
|
|
164
170
|
|
|
165
171
|
end = time.time()
|
|
166
172
|
table.add_row(
|
|
@@ -200,7 +206,7 @@ async def execute_preset_checks(preset_checks: list) -> (int, List[dict]):
|
|
|
200
206
|
return rc, failed_checks
|
|
201
207
|
|
|
202
208
|
|
|
203
|
-
async def execute_state_checks(checks:
|
|
209
|
+
async def execute_state_checks(checks: List, is_skip_query: bool) -> Tuple[int, List[Dict]]:
|
|
204
210
|
"""
|
|
205
211
|
Execute the checks from loaded state
|
|
206
212
|
"""
|
|
@@ -232,7 +238,7 @@ async def execute_state_checks(checks: list) -> (int, List[dict]):
|
|
|
232
238
|
raise ValueError(f"Invalid check type: {check_type}")
|
|
233
239
|
|
|
234
240
|
start = time.time()
|
|
235
|
-
if check_type not in ["schema_diff"]:
|
|
241
|
+
if check_type not in ["schema_diff", "lineage_diff"] and not is_skip_query:
|
|
236
242
|
run, future = submit_run(check_type, params=check_params, check_id=check_id)
|
|
237
243
|
await future
|
|
238
244
|
|
|
@@ -295,7 +301,7 @@ def process_failed_checks(failed_checks: List[dict], error_log=None):
|
|
|
295
301
|
content += markdown_table(failed_check_table).set_params(quote=False, row_sep="markdown").get_markdown()
|
|
296
302
|
|
|
297
303
|
if error_log:
|
|
298
|
-
with open(error_log, "w") as f:
|
|
304
|
+
with open(error_log, "w", encoding="utf-8") as f:
|
|
299
305
|
f.write(content)
|
|
300
306
|
print(f"The failed checks are stored at '{error_log}'")
|
|
301
307
|
else:
|
|
@@ -315,6 +321,7 @@ async def cli_run(output_state_file: str, **kwargs):
|
|
|
315
321
|
ctx = load_context(**kwargs)
|
|
316
322
|
|
|
317
323
|
is_skip_query = kwargs.get("skip_query", False)
|
|
324
|
+
is_skip_check = kwargs.get("skip_check", False)
|
|
318
325
|
|
|
319
326
|
# Prepare the artifact by collecting the lineage
|
|
320
327
|
console.rule("DBT Artifacts")
|
|
@@ -327,23 +334,23 @@ async def cli_run(output_state_file: str, **kwargs):
|
|
|
327
334
|
rc = 0
|
|
328
335
|
if ctx.state_loader.state is None:
|
|
329
336
|
preset_checks = RecceConfig().get("checks")
|
|
330
|
-
if
|
|
337
|
+
if is_skip_check or preset_checks is None or len(preset_checks) == 0:
|
|
331
338
|
# Skip the preset checks
|
|
332
339
|
pass
|
|
333
340
|
else:
|
|
334
341
|
console.rule("Preset checks")
|
|
335
|
-
_, failed_checks = await execute_preset_checks(preset_checks)
|
|
342
|
+
_, failed_checks = await execute_preset_checks(preset_checks, is_skip_query)
|
|
336
343
|
if failed_checks:
|
|
337
344
|
console.print("[[yellow]Warning[/yellow]] Preset checks failed. Please see the failed reason.")
|
|
338
345
|
process_failed_checks(failed_checks, error_log)
|
|
339
346
|
else:
|
|
340
347
|
state_checks = ctx.state_loader.state.checks
|
|
341
|
-
if
|
|
348
|
+
if is_skip_check or state_checks is None or len(state_checks) == 0:
|
|
342
349
|
# Skip the checks in the state
|
|
343
350
|
pass
|
|
344
351
|
else:
|
|
345
352
|
console.rule("Checks")
|
|
346
|
-
_, failed_checks = await execute_state_checks(state_checks)
|
|
353
|
+
_, failed_checks = await execute_state_checks(state_checks, is_skip_query)
|
|
347
354
|
if failed_checks:
|
|
348
355
|
console.print("[[yellow]Warning[/yellow]] Checks failed. Please see the failed reason.")
|
|
349
356
|
process_failed_checks(failed_checks, error_log)
|
|
@@ -363,7 +370,7 @@ async def cli_run(output_state_file: str, **kwargs):
|
|
|
363
370
|
dirs = os.path.dirname(summary_path)
|
|
364
371
|
if dirs:
|
|
365
372
|
os.makedirs(dirs, exist_ok=True)
|
|
366
|
-
with open(summary_path, "w") as f:
|
|
373
|
+
with open(summary_path, "w", encoding="utf-8") as f:
|
|
367
374
|
f.write(generate_markdown_summary(ctx))
|
|
368
375
|
console.print(f"The summary is stored at '{summary_path}'")
|
|
369
376
|
|
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,19 +30,44 @@ 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
|
|
34
35
|
from .apis.run_api import run_router
|
|
35
36
|
from .config import RecceConfig
|
|
37
|
+
from .connect_to_cloud import (
|
|
38
|
+
connect_to_cloud_background_task,
|
|
39
|
+
generate_key_pair,
|
|
40
|
+
get_connection_url,
|
|
41
|
+
is_callback_server_running,
|
|
42
|
+
prepare_connection_url,
|
|
43
|
+
)
|
|
36
44
|
from .core import RecceContext, default_context, load_context
|
|
37
|
-
from .event import log_api_event, log_single_env_event
|
|
45
|
+
from .event import get_recce_api_token, log_api_event, log_single_env_event
|
|
38
46
|
from .exceptions import RecceException
|
|
47
|
+
from .github import is_github_codespace
|
|
39
48
|
from .models.types import CllData
|
|
40
49
|
from .run import load_preset_checks
|
|
41
50
|
from .state import RecceShareStateManager, RecceStateLoader
|
|
42
51
|
|
|
43
52
|
logger = logging.getLogger("uvicorn")
|
|
44
53
|
|
|
54
|
+
# Idle timeout check interval bounds (in seconds)
|
|
55
|
+
MAX_CHECK_INTERVAL = 30
|
|
56
|
+
MIN_CHECK_INTERVAL = 1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RecceServerMode(str, Enum):
|
|
60
|
+
server = "server"
|
|
61
|
+
preview = "preview"
|
|
62
|
+
read_only = "read-only"
|
|
63
|
+
|
|
64
|
+
def __str__(self):
|
|
65
|
+
return self.value
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def available_members() -> Set[str]:
|
|
69
|
+
return ["server", "preview", "read-only"]
|
|
70
|
+
|
|
45
71
|
|
|
46
72
|
@dataclass
|
|
47
73
|
class AppState:
|
|
@@ -52,13 +78,19 @@ class AppState:
|
|
|
52
78
|
auth_options: Optional[dict] = None
|
|
53
79
|
lifetime: Optional[int] = None
|
|
54
80
|
lifetime_expired_at: Optional[datetime] = None
|
|
81
|
+
idle_timeout: Optional[int] = None
|
|
82
|
+
last_activity: Optional[dict] = None
|
|
55
83
|
share_url: Optional[str] = None
|
|
84
|
+
organization_name: Optional[str] = None
|
|
85
|
+
web_url: Optional[str] = None
|
|
86
|
+
host: Optional[str] = None
|
|
87
|
+
port: Optional[int] = None
|
|
56
88
|
|
|
57
89
|
|
|
58
90
|
def schedule_lifetime_termination(app_state):
|
|
59
91
|
def terminating_server():
|
|
60
92
|
pid = os.getpid()
|
|
61
|
-
logger.info(f"Terminating server process [{pid}] manually")
|
|
93
|
+
logger.info(f"Terminating server process [{pid}] manually due to lifetime expiration")
|
|
62
94
|
os.kill(pid, signal.SIGINT)
|
|
63
95
|
|
|
64
96
|
# Terminate the server process after the specified lifetime
|
|
@@ -67,6 +99,56 @@ def schedule_lifetime_termination(app_state):
|
|
|
67
99
|
asyncio.get_running_loop().call_later(app_state.lifetime, terminating_server)
|
|
68
100
|
|
|
69
101
|
|
|
102
|
+
def schedule_idle_timeout_check(app_state):
|
|
103
|
+
"""
|
|
104
|
+
Schedule periodic checks for idle timeout.
|
|
105
|
+
If the server has been idle for longer than idle_timeout, terminate it.
|
|
106
|
+
"""
|
|
107
|
+
# Track last activity time in app_state
|
|
108
|
+
app_state.last_activity = {"time": datetime.now(utc)}
|
|
109
|
+
|
|
110
|
+
def terminating_server_idle():
|
|
111
|
+
pid = os.getpid()
|
|
112
|
+
logger.info(f"Terminating server process [{pid}] manually due to idle timeout")
|
|
113
|
+
os.kill(pid, signal.SIGINT)
|
|
114
|
+
|
|
115
|
+
async def check_idle_timeout():
|
|
116
|
+
"""Periodically check if the server has been idle for too long"""
|
|
117
|
+
# Use smaller check interval if idle_timeout is very short
|
|
118
|
+
# Check at least every MAX_CHECK_INTERVAL seconds, but also check when idle_timeout is approaching
|
|
119
|
+
check_interval = min(MAX_CHECK_INTERVAL, max(MIN_CHECK_INTERVAL, app_state.idle_timeout // 3))
|
|
120
|
+
|
|
121
|
+
logger.debug(f"[Idle Timeout] Starting idle timeout checker with {check_interval}s check interval")
|
|
122
|
+
|
|
123
|
+
while True:
|
|
124
|
+
await asyncio.sleep(check_interval)
|
|
125
|
+
|
|
126
|
+
idle_seconds = (datetime.now(utc) - app_state.last_activity["time"]).total_seconds()
|
|
127
|
+
remaining_seconds = app_state.idle_timeout - idle_seconds
|
|
128
|
+
|
|
129
|
+
# Always log the countdown for debugging
|
|
130
|
+
if remaining_seconds > 0:
|
|
131
|
+
logger.debug(
|
|
132
|
+
f"[Idle Timeout] Server idle for {idle_seconds:.1f}s / {app_state.idle_timeout}s "
|
|
133
|
+
f"(remaining: {remaining_seconds:.1f}s)"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if idle_seconds >= app_state.idle_timeout:
|
|
137
|
+
logger.info(
|
|
138
|
+
f"[Idle Timeout] Threshold reached! Server has been idle for {idle_seconds:.0f} seconds "
|
|
139
|
+
f"(threshold: {app_state.idle_timeout} seconds)"
|
|
140
|
+
)
|
|
141
|
+
terminating_server_idle()
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
# Start the idle timeout check task
|
|
145
|
+
logger.info(f"[Configuration] The idle timeout of the server is {app_state.idle_timeout} seconds")
|
|
146
|
+
|
|
147
|
+
# Create task using asyncio.create_task which works in async context
|
|
148
|
+
task = asyncio.create_task(check_idle_timeout())
|
|
149
|
+
logger.debug(f"[Idle Timeout] Background task created: {task}")
|
|
150
|
+
|
|
151
|
+
|
|
70
152
|
def setup_server(app_state: AppState) -> RecceContext:
|
|
71
153
|
from rich.console import Console
|
|
72
154
|
|
|
@@ -96,39 +178,67 @@ def setup_server(app_state: AppState) -> RecceContext:
|
|
|
96
178
|
|
|
97
179
|
log_load_state(command="server", single_env=single_env)
|
|
98
180
|
|
|
99
|
-
if app_state.lifetime is not None and app_state.lifetime > 0:
|
|
100
|
-
schedule_lifetime_termination(app_state)
|
|
101
|
-
|
|
102
181
|
return ctx
|
|
103
182
|
|
|
104
183
|
|
|
105
184
|
def teardown_server(app_state: AppState, ctx: RecceContext):
|
|
106
|
-
|
|
185
|
+
# pull latest state, merge runs/checks and pick the newer artifacts
|
|
186
|
+
state_loader = ctx.state_loader
|
|
187
|
+
state_loader.refresh()
|
|
188
|
+
if state_loader.state:
|
|
189
|
+
ctx.import_state(state_loader.state, merge=True)
|
|
107
190
|
state_loader.export(ctx.export_state())
|
|
108
|
-
|
|
109
191
|
ctx.stop_monitor_artifacts()
|
|
110
192
|
if app_state.flag.get("single_env_onboarding", False):
|
|
111
193
|
ctx.stop_monitor_base_env()
|
|
112
194
|
|
|
113
195
|
|
|
114
196
|
def setup_ready_only(app_state: AppState):
|
|
115
|
-
|
|
116
|
-
schedule_lifetime_termination(app_state)
|
|
197
|
+
pass
|
|
117
198
|
|
|
118
199
|
|
|
119
200
|
def teardown_ready_only(app_state: AppState):
|
|
120
201
|
pass
|
|
121
202
|
|
|
122
203
|
|
|
204
|
+
def setup_preview(app_state: AppState):
|
|
205
|
+
state_loader = app_state.state_loader
|
|
206
|
+
kwargs = app_state.kwargs
|
|
207
|
+
ctx = load_context(**kwargs, state_loader=state_loader)
|
|
208
|
+
return ctx
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def teardown_preview(app_state: AppState, ctx: RecceContext):
|
|
212
|
+
state_loader = app_state.state_loader
|
|
213
|
+
state_loader.export(ctx.export_state())
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
|
|
123
217
|
@asynccontextmanager
|
|
124
218
|
async def lifespan(fastapi: FastAPI):
|
|
125
219
|
ctx = None
|
|
126
220
|
app_state: AppState = app.state
|
|
127
221
|
|
|
222
|
+
# Ensure logger is at DEBUG level if debug mode is enabled
|
|
223
|
+
# This is needed because uvicorn might reset logger configuration
|
|
224
|
+
if hasattr(app_state, "kwargs") and app_state.kwargs.get("debug"):
|
|
225
|
+
logger.setLevel(logging.DEBUG)
|
|
226
|
+
logger.debug("Debug mode enabled - logger set to DEBUG level")
|
|
227
|
+
|
|
128
228
|
if app_state.command == "server":
|
|
129
229
|
ctx = setup_server(app_state)
|
|
130
|
-
elif app_state.command == "
|
|
230
|
+
elif app_state.command == "read-only":
|
|
131
231
|
setup_ready_only(app_state)
|
|
232
|
+
elif app_state.command == "preview":
|
|
233
|
+
ctx = setup_preview(app_state)
|
|
234
|
+
|
|
235
|
+
if app_state.lifetime is not None and app_state.lifetime > 0:
|
|
236
|
+
schedule_lifetime_termination(app_state)
|
|
237
|
+
|
|
238
|
+
# Schedule idle timeout check if idle_timeout is set
|
|
239
|
+
if app_state.idle_timeout is not None and app_state.idle_timeout > 0:
|
|
240
|
+
logger.debug(f"[Idle Timeout] Scheduling idle timeout check with {app_state.idle_timeout} seconds")
|
|
241
|
+
schedule_idle_timeout_check(app_state)
|
|
132
242
|
|
|
133
243
|
yield
|
|
134
244
|
|
|
@@ -136,6 +246,8 @@ async def lifespan(fastapi: FastAPI):
|
|
|
136
246
|
teardown_server(app_state, ctx)
|
|
137
247
|
elif app_state.command == "read_only":
|
|
138
248
|
teardown_ready_only(app_state)
|
|
249
|
+
elif app_state.command == "preview":
|
|
250
|
+
teardown_preview(app_state, ctx)
|
|
139
251
|
|
|
140
252
|
|
|
141
253
|
app = FastAPI(lifespan=lifespan)
|
|
@@ -143,7 +255,7 @@ app = FastAPI(lifespan=lifespan)
|
|
|
143
255
|
|
|
144
256
|
def verify_json_file(file_path: str) -> bool:
|
|
145
257
|
try:
|
|
146
|
-
with open(file_path, "r") as f:
|
|
258
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
147
259
|
json.load(f)
|
|
148
260
|
except Exception:
|
|
149
261
|
return False
|
|
@@ -200,6 +312,27 @@ app.add_middleware(
|
|
|
200
312
|
)
|
|
201
313
|
|
|
202
314
|
|
|
315
|
+
@app.middleware("http")
|
|
316
|
+
async def track_activity_for_idle_timeout(request: Request, call_next):
|
|
317
|
+
"""Track activity time for idle timeout check"""
|
|
318
|
+
# Exclude paths that should not reset idle timer
|
|
319
|
+
# Health checks and monitoring endpoints don't count as user activity
|
|
320
|
+
excluded_paths = ["/api/health", "/api/ws"]
|
|
321
|
+
|
|
322
|
+
# Update last activity time BEFORE processing request if idle timeout is enabled
|
|
323
|
+
# This ensures long-running requests don't get terminated mid-execution
|
|
324
|
+
app_state: AppState = app.state
|
|
325
|
+
if app_state.last_activity is not None:
|
|
326
|
+
if request.url.path not in excluded_paths:
|
|
327
|
+
app_state.last_activity["time"] = datetime.now(utc)
|
|
328
|
+
logger.debug(f"[Idle Timeout] ✓ Activity detected: {request.method} {request.url.path} - Timer reset")
|
|
329
|
+
else:
|
|
330
|
+
logger.debug(f"[Idle Timeout] Excluded path (no timer reset): {request.method} {request.url.path}")
|
|
331
|
+
|
|
332
|
+
response = await call_next(request)
|
|
333
|
+
return response
|
|
334
|
+
|
|
335
|
+
|
|
203
336
|
@app.middleware("http")
|
|
204
337
|
async def set_context_by_cookie(request: Request, call_next):
|
|
205
338
|
response = await call_next(request)
|
|
@@ -233,11 +366,17 @@ async def health_check(request: Request):
|
|
|
233
366
|
|
|
234
367
|
|
|
235
368
|
class RecceInstanceInfoOut(BaseModel):
|
|
369
|
+
server_mode: RecceServerMode
|
|
236
370
|
read_only: bool
|
|
371
|
+
preview: bool
|
|
237
372
|
single_env: bool
|
|
238
373
|
authed: bool
|
|
374
|
+
cloud_instance: bool
|
|
239
375
|
lifetime_expired_at: Optional[datetime] = None
|
|
240
376
|
share_url: Optional[str] = None
|
|
377
|
+
session_id: Optional[str] = None
|
|
378
|
+
organization_name: Optional[str] = None
|
|
379
|
+
web_url: Optional[str] = None
|
|
241
380
|
|
|
242
381
|
|
|
243
382
|
@app.get("/api/instance-info", response_model=RecceInstanceInfoOut, response_model_exclude_none=True)
|
|
@@ -247,15 +386,20 @@ async def recce_instance_info():
|
|
|
247
386
|
read_only = flag.get("read_only", False)
|
|
248
387
|
single_env = flag.get("single_env_onboarding", False)
|
|
249
388
|
|
|
250
|
-
|
|
251
|
-
api_token = auth_options.get("api_token")
|
|
389
|
+
api_token = get_recce_api_token()
|
|
252
390
|
|
|
253
391
|
return {
|
|
392
|
+
"server_mode": app_state.command,
|
|
254
393
|
"read_only": read_only,
|
|
394
|
+
"preview": flag.get("preview", False),
|
|
255
395
|
"single_env": single_env,
|
|
256
396
|
"authed": True if api_token else False,
|
|
397
|
+
"cloud_instance": is_recce_cloud_instance(),
|
|
257
398
|
"lifetime_expired_at": app_state.lifetime_expired_at, # UTC timezone
|
|
258
399
|
"share_url": app_state.share_url,
|
|
400
|
+
"session_id": app_state.state_loader.session_id if app_state.state_loader else None,
|
|
401
|
+
"organization_name": app_state.organization_name,
|
|
402
|
+
"web_url": app_state.web_url,
|
|
259
403
|
# TODO: Add more instance info which won't change during the instance lifecycle
|
|
260
404
|
# review_mode
|
|
261
405
|
# cloud_mode
|
|
@@ -283,6 +427,7 @@ async def get_info():
|
|
|
283
427
|
"""
|
|
284
428
|
context = default_context()
|
|
285
429
|
demo = os.environ.get("DEMO", False)
|
|
430
|
+
is_codespace = is_github_codespace()
|
|
286
431
|
|
|
287
432
|
if demo:
|
|
288
433
|
state = context.export_demo_state()
|
|
@@ -307,6 +452,7 @@ async def get_info():
|
|
|
307
452
|
"pull_request": state.pull_request.to_dict() if state.pull_request else None,
|
|
308
453
|
"lineage": lineage_diff,
|
|
309
454
|
"demo": bool(demo),
|
|
455
|
+
"codespace": bool(is_codespace),
|
|
310
456
|
"cloud_mode": context.state_loader.cloud_mode,
|
|
311
457
|
"file_mode": context.state_loader.state_file is not None,
|
|
312
458
|
"filename": filename,
|
|
@@ -331,9 +477,9 @@ class CllIn(BaseModel):
|
|
|
331
477
|
node_id: Optional[str] = None
|
|
332
478
|
column: Optional[str] = None
|
|
333
479
|
change_analysis: Optional[bool] = False
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
480
|
+
no_cll: Optional[bool] = False
|
|
481
|
+
no_upstream: Optional[bool] = False
|
|
482
|
+
no_downstream: Optional[bool] = False
|
|
337
483
|
|
|
338
484
|
|
|
339
485
|
class CllOutput(BaseModel):
|
|
@@ -349,8 +495,9 @@ async def column_level_lineage_by_node(cll_input: CllIn):
|
|
|
349
495
|
node_id=cll_input.node_id,
|
|
350
496
|
column=cll_input.column,
|
|
351
497
|
change_analysis=cll_input.change_analysis,
|
|
352
|
-
|
|
353
|
-
|
|
498
|
+
no_upstream=cll_input.no_upstream,
|
|
499
|
+
no_downstream=cll_input.no_downstream,
|
|
500
|
+
no_cll=cll_input.no_cll,
|
|
354
501
|
)
|
|
355
502
|
|
|
356
503
|
return CllOutput(current=cll)
|
|
@@ -654,6 +801,34 @@ async def broadcast(data: str):
|
|
|
654
801
|
await client.send_text(data)
|
|
655
802
|
|
|
656
803
|
|
|
804
|
+
@app.post("/api/connect")
|
|
805
|
+
async def generate_connect_to_cloud_url(background_tasks: BackgroundTasks):
|
|
806
|
+
if is_callback_server_running():
|
|
807
|
+
return {"connection_url": get_connection_url()}
|
|
808
|
+
|
|
809
|
+
private_key, public_key = generate_key_pair()
|
|
810
|
+
connection_url, callback_port = prepare_connection_url(public_key)
|
|
811
|
+
|
|
812
|
+
background_tasks.add_task(connect_to_cloud_background_task, private_key, callback_port, connection_url)
|
|
813
|
+
return {
|
|
814
|
+
"connection_url": connection_url,
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
@app.get("/api/users")
|
|
819
|
+
async def get_user_info():
|
|
820
|
+
from recce.connect_to_cloud import RecceCloud
|
|
821
|
+
|
|
822
|
+
context = default_context()
|
|
823
|
+
user_token = get_recce_api_token() or context.state_loader.token
|
|
824
|
+
cloud = RecceCloud(user_token)
|
|
825
|
+
try:
|
|
826
|
+
user_info = cloud.get_user_info()
|
|
827
|
+
return user_info
|
|
828
|
+
except Exception as e:
|
|
829
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
830
|
+
|
|
831
|
+
|
|
657
832
|
api_prefix = "/api"
|
|
658
833
|
app.include_router(check_router, prefix=api_prefix)
|
|
659
834
|
app.include_router(run_router, prefix=api_prefix)
|
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
|
+
]
|