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
recce/server.py
CHANGED
|
@@ -2,49 +2,161 @@ import asyncio
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import signal
|
|
5
6
|
import uuid
|
|
6
7
|
from contextlib import asynccontextmanager
|
|
7
8
|
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timedelta
|
|
10
|
+
from enum import Enum
|
|
8
11
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
10
|
-
|
|
11
|
-
from fastapi import
|
|
12
|
+
from typing import Annotated, Any, Literal, Optional, Set
|
|
13
|
+
|
|
14
|
+
from fastapi import (
|
|
15
|
+
BackgroundTasks,
|
|
16
|
+
FastAPI,
|
|
17
|
+
Form,
|
|
18
|
+
HTTPException,
|
|
19
|
+
Request,
|
|
20
|
+
Response,
|
|
21
|
+
UploadFile,
|
|
22
|
+
WebSocket,
|
|
23
|
+
)
|
|
12
24
|
from fastapi.middleware.cors import CORSMiddleware
|
|
13
25
|
from fastapi.responses import PlainTextResponse
|
|
14
26
|
from fastapi.staticfiles import StaticFiles
|
|
15
|
-
from pydantic import
|
|
27
|
+
from pydantic import BaseModel, ValidationError
|
|
28
|
+
from pytz import utc
|
|
16
29
|
from starlette.middleware.gzip import GZipMiddleware
|
|
17
30
|
from starlette.middleware.sessions import SessionMiddleware
|
|
18
31
|
from starlette.websockets import WebSocketDisconnect
|
|
19
32
|
|
|
20
|
-
from . import __version__, event,
|
|
33
|
+
from . import __latest_version__, __version__, event, is_recce_cloud_instance
|
|
21
34
|
from .apis.check_api import check_router
|
|
35
|
+
from .apis.check_events_api import check_events_router
|
|
22
36
|
from .apis.run_api import run_router
|
|
23
37
|
from .config import RecceConfig
|
|
24
|
-
from .
|
|
25
|
-
|
|
38
|
+
from .connect_to_cloud import (
|
|
39
|
+
connect_to_cloud_background_task,
|
|
40
|
+
generate_key_pair,
|
|
41
|
+
get_connection_url,
|
|
42
|
+
is_callback_server_running,
|
|
43
|
+
prepare_connection_url,
|
|
44
|
+
)
|
|
45
|
+
from .core import RecceContext, default_context, load_context
|
|
46
|
+
from .event import get_recce_api_token, log_api_event, log_single_env_event
|
|
26
47
|
from .exceptions import RecceException
|
|
48
|
+
from .github import is_github_codespace
|
|
49
|
+
from .models.types import CllData
|
|
27
50
|
from .run import load_preset_checks
|
|
28
|
-
from .state import
|
|
51
|
+
from .state import RecceShareStateManager, RecceStateLoader
|
|
52
|
+
from .util.startup_perf import track_timing
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger("uvicorn")
|
|
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"
|
|
29
65
|
|
|
30
|
-
|
|
66
|
+
def __str__(self):
|
|
67
|
+
return self.value
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def available_members() -> Set[str]:
|
|
71
|
+
return ["server", "preview", "read-only"]
|
|
31
72
|
|
|
32
73
|
|
|
33
74
|
@dataclass
|
|
34
75
|
class AppState:
|
|
76
|
+
command: Optional[str] = None
|
|
35
77
|
state_loader: Optional[RecceStateLoader] = None
|
|
36
78
|
kwargs: Optional[dict] = None
|
|
37
79
|
flag: Optional[dict] = None
|
|
38
80
|
auth_options: Optional[dict] = None
|
|
81
|
+
lifetime: Optional[int] = None
|
|
82
|
+
lifetime_expired_at: Optional[datetime] = None
|
|
83
|
+
idle_timeout: Optional[int] = None
|
|
84
|
+
last_activity: Optional[dict] = None
|
|
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
|
|
39
90
|
|
|
40
91
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
92
|
+
def schedule_lifetime_termination(app_state):
|
|
93
|
+
def terminating_server():
|
|
94
|
+
pid = os.getpid()
|
|
95
|
+
logger.info(f"Terminating server process [{pid}] manually due to lifetime expiration")
|
|
96
|
+
os.kill(pid, signal.SIGINT)
|
|
97
|
+
|
|
98
|
+
# Terminate the server process after the specified lifetime
|
|
99
|
+
logger.info(f"[Configuration] The lifetime of the server is {app_state.lifetime} seconds")
|
|
100
|
+
app.state.lifetime_expired_at = datetime.now(utc) + timedelta(seconds=app_state.lifetime)
|
|
101
|
+
asyncio.get_running_loop().call_later(app_state.lifetime, terminating_server)
|
|
102
|
+
|
|
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
|
+
|
|
154
|
+
def setup_server(app_state: AppState) -> RecceContext:
|
|
44
155
|
from rich.console import Console
|
|
45
156
|
|
|
157
|
+
from .core import load_context
|
|
158
|
+
|
|
46
159
|
console = Console()
|
|
47
|
-
app_state: AppState = app.state
|
|
48
160
|
state_loader = app_state.state_loader
|
|
49
161
|
kwargs = app_state.kwargs
|
|
50
162
|
ctx = load_context(**kwargs, state_loader=state_loader)
|
|
@@ -57,31 +169,118 @@ async def lifespan(fastapi: FastAPI):
|
|
|
57
169
|
log_single_env_event()
|
|
58
170
|
|
|
59
171
|
# Initialize Recce Config
|
|
60
|
-
config = RecceConfig(config_file=kwargs.get(
|
|
172
|
+
config = RecceConfig(config_file=kwargs.get("config"))
|
|
61
173
|
if state_loader.state is None:
|
|
62
|
-
preset_checks = config.get(
|
|
174
|
+
preset_checks = config.get("checks", [])
|
|
63
175
|
if preset_checks and len(preset_checks) > 0:
|
|
64
176
|
console.rule("Loading Preset Checks")
|
|
65
177
|
load_preset_checks(preset_checks)
|
|
66
178
|
|
|
67
179
|
from recce.event import log_load_state
|
|
68
|
-
log_load_state(command='server', single_env=single_env)
|
|
69
180
|
|
|
70
|
-
|
|
181
|
+
log_load_state(command="server", single_env=single_env)
|
|
182
|
+
|
|
183
|
+
return ctx
|
|
71
184
|
|
|
72
|
-
state_loader.export(ctx.export_state())
|
|
73
185
|
|
|
186
|
+
def teardown_server(app_state: AppState, ctx: RecceContext):
|
|
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)
|
|
192
|
+
state_loader.export(ctx.export_state())
|
|
74
193
|
ctx.stop_monitor_artifacts()
|
|
75
194
|
if app_state.flag.get("single_env_onboarding", False):
|
|
76
195
|
ctx.stop_monitor_base_env()
|
|
77
196
|
|
|
78
197
|
|
|
198
|
+
def setup_ready_only(app_state: AppState):
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def teardown_ready_only(app_state: AppState):
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
|
|
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
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@track_timing("server_setup")
|
|
220
|
+
def _do_lifespan_setup(app_state: AppState):
|
|
221
|
+
"""Run server setup and return context for teardown."""
|
|
222
|
+
if app_state.command == "server":
|
|
223
|
+
ctx = setup_server(app_state)
|
|
224
|
+
elif app_state.command == "read-only":
|
|
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()
|
|
267
|
+
|
|
268
|
+
yield
|
|
269
|
+
|
|
270
|
+
if app_state.command == "server":
|
|
271
|
+
teardown_server(app_state, ctx)
|
|
272
|
+
elif app_state.command == "read-only":
|
|
273
|
+
teardown_ready_only(app_state)
|
|
274
|
+
elif app_state.command == "preview":
|
|
275
|
+
teardown_preview(app_state, ctx)
|
|
276
|
+
|
|
277
|
+
|
|
79
278
|
app = FastAPI(lifespan=lifespan)
|
|
80
279
|
|
|
81
280
|
|
|
82
281
|
def verify_json_file(file_path: str) -> bool:
|
|
83
282
|
try:
|
|
84
|
-
with open(file_path,
|
|
283
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
85
284
|
json.load(f)
|
|
86
285
|
except Exception:
|
|
87
286
|
return False
|
|
@@ -94,19 +293,15 @@ def dbt_artifacts_updated_callback(file_changed_event: Any):
|
|
|
94
293
|
file_name = src_path.name
|
|
95
294
|
|
|
96
295
|
if not verify_json_file(file_changed_event.src_path):
|
|
97
|
-
logger.debug(
|
|
296
|
+
logger.debug("Skip to refresh the artifacts because the file is not updated completely.")
|
|
98
297
|
return
|
|
99
298
|
|
|
100
|
-
logger.info(
|
|
101
|
-
f'Detect {target_type} file {file_changed_event.event_type}: {file_name}')
|
|
299
|
+
logger.info(f"Detect {target_type} file {file_changed_event.event_type}: {file_name}")
|
|
102
300
|
ctx = load_context()
|
|
103
301
|
ctx.refresh_manifest(file_changed_event.src_path)
|
|
104
302
|
broadcast_command = {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
'eventType': file_changed_event.event_type,
|
|
108
|
-
'srcPath': file_changed_event.src_path
|
|
109
|
-
}
|
|
303
|
+
"command": "refresh",
|
|
304
|
+
"event": {"eventType": file_changed_event.event_type, "srcPath": file_changed_event.src_path},
|
|
110
305
|
}
|
|
111
306
|
payload = json.dumps(broadcast_command)
|
|
112
307
|
asyncio.run(broadcast(payload))
|
|
@@ -115,7 +310,7 @@ def dbt_artifacts_updated_callback(file_changed_event: Any):
|
|
|
115
310
|
def dbt_env_updated_callback():
|
|
116
311
|
logger.info("Detect 'manifest.json' and 'catalog.json' are generated under 'target-base' directory")
|
|
117
312
|
broadcast_command = {
|
|
118
|
-
|
|
313
|
+
"command": "relaunch",
|
|
119
314
|
}
|
|
120
315
|
payload = json.dumps(broadcast_command)
|
|
121
316
|
asyncio.run(broadcast(payload))
|
|
@@ -142,11 +337,32 @@ app.add_middleware(
|
|
|
142
337
|
)
|
|
143
338
|
|
|
144
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
|
+
|
|
145
361
|
@app.middleware("http")
|
|
146
362
|
async def set_context_by_cookie(request: Request, call_next):
|
|
147
363
|
response = await call_next(request)
|
|
148
364
|
|
|
149
|
-
user_id_in_cookie = request.cookies.get(
|
|
365
|
+
user_id_in_cookie = request.cookies.get("recce_user_id")
|
|
150
366
|
user_id = event.get_user_id()
|
|
151
367
|
|
|
152
368
|
if event.is_anonymous_tracking() is False:
|
|
@@ -154,7 +370,7 @@ async def set_context_by_cookie(request: Request, call_next):
|
|
|
154
370
|
user_id = None
|
|
155
371
|
|
|
156
372
|
if user_id_in_cookie is None or user_id_in_cookie != user_id:
|
|
157
|
-
response.set_cookie(key=
|
|
373
|
+
response.set_cookie(key="recce_user_id", value=user_id)
|
|
158
374
|
return response
|
|
159
375
|
|
|
160
376
|
|
|
@@ -163,8 +379,8 @@ async def disable_cache(request: Request, call_next):
|
|
|
163
379
|
response = await call_next(request)
|
|
164
380
|
|
|
165
381
|
# disable cache for '/' and '/index.html'
|
|
166
|
-
if request.url.path in [
|
|
167
|
-
response.headers[
|
|
382
|
+
if request.url.path in ["/", "/index.html"]:
|
|
383
|
+
response.headers["Cache-Control"] = "no-store"
|
|
168
384
|
|
|
169
385
|
return response
|
|
170
386
|
|
|
@@ -174,18 +390,54 @@ async def health_check(request: Request):
|
|
|
174
390
|
return {"status": "ok"}
|
|
175
391
|
|
|
176
392
|
|
|
177
|
-
@app.
|
|
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
|
+
|
|
404
|
+
class RecceInstanceInfoOut(BaseModel):
|
|
405
|
+
server_mode: RecceServerMode
|
|
406
|
+
read_only: bool
|
|
407
|
+
preview: bool
|
|
408
|
+
single_env: bool
|
|
409
|
+
authed: bool
|
|
410
|
+
cloud_instance: bool
|
|
411
|
+
lifetime_expired_at: Optional[datetime] = None
|
|
412
|
+
idle_timeout: Optional[int] = None
|
|
413
|
+
share_url: Optional[str] = None
|
|
414
|
+
session_id: Optional[str] = None
|
|
415
|
+
organization_name: Optional[str] = None
|
|
416
|
+
web_url: Optional[str] = None
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@app.get("/api/instance-info", response_model=RecceInstanceInfoOut, response_model_exclude_none=True)
|
|
178
420
|
async def recce_instance_info():
|
|
179
421
|
app_state: AppState = app.state
|
|
180
422
|
flag = app_state.flag
|
|
181
|
-
read_only = flag.get(
|
|
423
|
+
read_only = flag.get("read_only", False)
|
|
424
|
+
single_env = flag.get("single_env_onboarding", False)
|
|
182
425
|
|
|
183
|
-
|
|
184
|
-
api_token = auth_options.get('api_token')
|
|
426
|
+
api_token = get_recce_api_token()
|
|
185
427
|
|
|
186
428
|
return {
|
|
429
|
+
"server_mode": app_state.command,
|
|
187
430
|
"read_only": read_only,
|
|
431
|
+
"preview": flag.get("preview", False),
|
|
432
|
+
"single_env": single_env,
|
|
188
433
|
"authed": True if api_token else False,
|
|
434
|
+
"cloud_instance": is_recce_cloud_instance(),
|
|
435
|
+
"lifetime_expired_at": app_state.lifetime_expired_at, # UTC timezone
|
|
436
|
+
"idle_timeout": app_state.idle_timeout,
|
|
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,
|
|
189
441
|
# TODO: Add more instance info which won't change during the instance lifecycle
|
|
190
442
|
# review_mode
|
|
191
443
|
# cloud_mode
|
|
@@ -201,16 +453,9 @@ async def config_flag():
|
|
|
201
453
|
return flag
|
|
202
454
|
|
|
203
455
|
|
|
204
|
-
@app.post("/api/onboarding/completed", status_code=204)
|
|
205
|
-
async def mark_onboarding_completed():
|
|
206
|
-
context = default_context()
|
|
207
|
-
context.mark_onboarding_completed()
|
|
208
|
-
app.state.flag['show_onboarding_guide'] = False
|
|
209
|
-
|
|
210
|
-
|
|
211
456
|
@app.post("/api/relaunch-hint/completed", status_code=204)
|
|
212
457
|
async def mark_relaunch_hint_completed():
|
|
213
|
-
app.state.flag[
|
|
458
|
+
app.state.flag["show_relaunch_hint"] = False
|
|
214
459
|
|
|
215
460
|
|
|
216
461
|
@app.get("/api/info")
|
|
@@ -219,7 +464,8 @@ async def get_info():
|
|
|
219
464
|
Get the information of the current context.
|
|
220
465
|
"""
|
|
221
466
|
context = default_context()
|
|
222
|
-
demo = os.environ.get(
|
|
467
|
+
demo = os.environ.get("DEMO", False)
|
|
468
|
+
is_codespace = is_github_codespace()
|
|
223
469
|
|
|
224
470
|
if demo:
|
|
225
471
|
state = context.export_demo_state()
|
|
@@ -232,28 +478,32 @@ async def get_info():
|
|
|
232
478
|
else:
|
|
233
479
|
filename = None
|
|
234
480
|
|
|
481
|
+
state_metadata = context.state_loader.state.metadata if context.state_loader.state else None
|
|
235
482
|
lineage_diff = context.get_lineage_diff()
|
|
236
483
|
|
|
237
484
|
try:
|
|
238
485
|
info = {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
486
|
+
"state_metadata": state_metadata,
|
|
487
|
+
"adapter_type": context.adapter_type,
|
|
488
|
+
"review_mode": context.review_mode,
|
|
489
|
+
"git": state.git.to_dict() if state.git else None,
|
|
490
|
+
"pull_request": state.pull_request.to_dict() if state.pull_request else None,
|
|
491
|
+
"lineage": lineage_diff,
|
|
492
|
+
"demo": bool(demo),
|
|
493
|
+
"codespace": bool(is_codespace),
|
|
494
|
+
"cloud_mode": context.state_loader.cloud_mode,
|
|
495
|
+
"file_mode": context.state_loader.state_file is not None,
|
|
496
|
+
"filename": filename,
|
|
497
|
+
"support_tasks": support_tasks,
|
|
249
498
|
}
|
|
250
499
|
|
|
251
|
-
if context.adapter_type ==
|
|
500
|
+
if context.adapter_type == "sqlmesh":
|
|
252
501
|
from recce.adapter.sqlmesh_adapter import SqlmeshAdapter
|
|
502
|
+
|
|
253
503
|
sqlmesh_adapter: SqlmeshAdapter = context.adapter
|
|
254
|
-
info[
|
|
255
|
-
|
|
256
|
-
|
|
504
|
+
info["sqlmesh"] = {
|
|
505
|
+
"base_env": sqlmesh_adapter.base_env.name,
|
|
506
|
+
"current_env": sqlmesh_adapter.curr_env.name,
|
|
257
507
|
}
|
|
258
508
|
|
|
259
509
|
return info
|
|
@@ -262,32 +512,40 @@ async def get_info():
|
|
|
262
512
|
|
|
263
513
|
|
|
264
514
|
class CllIn(BaseModel):
|
|
265
|
-
|
|
515
|
+
node_id: Optional[str] = None
|
|
516
|
+
column: Optional[str] = None
|
|
517
|
+
change_analysis: Optional[bool] = False
|
|
518
|
+
no_cll: Optional[bool] = False
|
|
519
|
+
no_upstream: Optional[bool] = False
|
|
520
|
+
no_downstream: Optional[bool] = False
|
|
266
521
|
|
|
267
522
|
|
|
268
523
|
class CllOutput(BaseModel):
|
|
269
|
-
current:
|
|
524
|
+
current: CllData
|
|
270
525
|
|
|
271
526
|
|
|
272
527
|
@app.post("/api/cll", response_model=CllOutput)
|
|
273
528
|
async def column_level_lineage_by_node(cll_input: CllIn):
|
|
274
529
|
from recce.adapter.dbt_adapter import DbtAdapter
|
|
275
|
-
dbt_adapter: DbtAdapter = default_context().adapter
|
|
276
530
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
531
|
+
dbt_adapter: DbtAdapter = default_context().adapter
|
|
532
|
+
cll = dbt_adapter.get_cll(
|
|
533
|
+
node_id=cll_input.node_id,
|
|
534
|
+
column=cll_input.column,
|
|
535
|
+
change_analysis=cll_input.change_analysis,
|
|
536
|
+
no_upstream=cll_input.no_upstream,
|
|
537
|
+
no_downstream=cll_input.no_downstream,
|
|
538
|
+
no_cll=cll_input.no_cll,
|
|
539
|
+
)
|
|
282
540
|
|
|
283
|
-
return CllOutput(current=
|
|
541
|
+
return CllOutput(current=cll)
|
|
284
542
|
|
|
285
543
|
|
|
286
544
|
class SelectNodesInput(BaseModel):
|
|
287
545
|
select: Optional[str] = None
|
|
288
546
|
exclude: Optional[str] = None
|
|
289
547
|
packages: Optional[list[str]] = None
|
|
290
|
-
view_mode: Optional[Literal[
|
|
548
|
+
view_mode: Optional[Literal["all", "changed_models"]] = None
|
|
291
549
|
|
|
292
550
|
|
|
293
551
|
class SelectNodesOutput(BaseModel):
|
|
@@ -298,8 +556,8 @@ class SelectNodesOutput(BaseModel):
|
|
|
298
556
|
async def select_nodes(input: SelectNodesInput):
|
|
299
557
|
context = default_context()
|
|
300
558
|
|
|
301
|
-
if context.adapter_type !=
|
|
302
|
-
raise HTTPException(status_code=400, detail=
|
|
559
|
+
if context.adapter_type != "dbt":
|
|
560
|
+
raise HTTPException(status_code=400, detail="Only dbt adapter is supported")
|
|
303
561
|
|
|
304
562
|
try:
|
|
305
563
|
nodes = context.adapter.select_nodes(
|
|
@@ -308,7 +566,7 @@ async def select_nodes(input: SelectNodesInput):
|
|
|
308
566
|
packages=input.packages,
|
|
309
567
|
view_mode=input.view_mode,
|
|
310
568
|
)
|
|
311
|
-
nodes = [node for node in nodes if not node.startswith(
|
|
569
|
+
nodes = [node for node in nodes if not node.startswith("test.")]
|
|
312
570
|
return SelectNodesOutput(nodes=nodes)
|
|
313
571
|
except Exception as e:
|
|
314
572
|
raise HTTPException(status_code=400, detail=str(e))
|
|
@@ -319,9 +577,9 @@ async def get_columns(model_id: str):
|
|
|
319
577
|
context = default_context()
|
|
320
578
|
try:
|
|
321
579
|
return {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
580
|
+
"model": {
|
|
581
|
+
"base": context.get_model(model_id, base=True),
|
|
582
|
+
"current": context.get_model(model_id, base=False),
|
|
325
583
|
}
|
|
326
584
|
}
|
|
327
585
|
except Exception as e:
|
|
@@ -336,12 +594,12 @@ async def save_handler():
|
|
|
336
594
|
try:
|
|
337
595
|
# Sync the state file
|
|
338
596
|
context = default_context()
|
|
339
|
-
log_api_event(
|
|
597
|
+
log_api_event("save", dict(state_loader_mode=context.state_loader_mode()))
|
|
340
598
|
state_loader = context.state_loader
|
|
341
599
|
if not state_loader.cloud_mode and state_loader.state_file is None:
|
|
342
|
-
raise RecceException(
|
|
600
|
+
raise RecceException("Not file mode or cloud mode")
|
|
343
601
|
|
|
344
|
-
context.sync_state(
|
|
602
|
+
context.sync_state("overwrite")
|
|
345
603
|
except RecceException as e:
|
|
346
604
|
raise HTTPException(status_code=400, detail=e.message)
|
|
347
605
|
|
|
@@ -357,33 +615,33 @@ def saveas_or_rename(input: SaveAsOrRenameInput, rename: bool = False):
|
|
|
357
615
|
context = default_context()
|
|
358
616
|
state_loader = context.state_loader
|
|
359
617
|
if state_loader.cloud_mode:
|
|
360
|
-
raise RecceException(
|
|
618
|
+
raise RecceException("Cloud mode does not support rename")
|
|
361
619
|
|
|
362
620
|
new_filename = input.filename
|
|
363
621
|
if os.path.dirname(new_filename):
|
|
364
|
-
raise RecceException(
|
|
365
|
-
if not new_filename.endswith(
|
|
366
|
-
raise RecceException(
|
|
622
|
+
raise RecceException("The new filename should not contain directory")
|
|
623
|
+
if not new_filename.endswith(".json"):
|
|
624
|
+
raise RecceException("The new filename should end with .json")
|
|
367
625
|
|
|
368
626
|
old_path = state_loader.state_file
|
|
369
627
|
if old_path:
|
|
370
628
|
old_dir = os.path.dirname(state_loader.state_file)
|
|
371
629
|
old_filename = os.path.basename(state_loader.state_file)
|
|
372
630
|
if old_filename == new_filename:
|
|
373
|
-
raise RecceException(
|
|
631
|
+
raise RecceException("The new filename is the same as the current filename")
|
|
374
632
|
new_path = os.path.join(old_dir, new_filename)
|
|
375
633
|
else:
|
|
376
634
|
new_path = new_filename
|
|
377
635
|
|
|
378
636
|
if os.path.exists(new_path):
|
|
379
637
|
if os.path.isdir(new_path):
|
|
380
|
-
raise HTTPException(status_code=400, detail=f
|
|
638
|
+
raise HTTPException(status_code=400, detail=f"The file {new_path} exists and is a directory")
|
|
381
639
|
|
|
382
640
|
if not input.overwrite:
|
|
383
|
-
raise HTTPException(status_code=409, detail=f
|
|
641
|
+
raise HTTPException(status_code=409, detail=f"The file {new_filename} already exists")
|
|
384
642
|
|
|
385
643
|
state_loader.state_file = new_path
|
|
386
|
-
context.sync_state(
|
|
644
|
+
context.sync_state("overwrite")
|
|
387
645
|
if rename and os.path.exists(old_path):
|
|
388
646
|
os.remove(old_path)
|
|
389
647
|
|
|
@@ -395,7 +653,7 @@ async def save_as_handler(input: SaveAsOrRenameInput):
|
|
|
395
653
|
"""
|
|
396
654
|
context = default_context()
|
|
397
655
|
try:
|
|
398
|
-
log_api_event(
|
|
656
|
+
log_api_event("saveas", dict(state_loader_mode=context.state_loader_mode()))
|
|
399
657
|
saveas_or_rename(input, rename=False)
|
|
400
658
|
except RecceException as e:
|
|
401
659
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -408,7 +666,7 @@ async def rename_handler(input: SaveAsOrRenameInput):
|
|
|
408
666
|
"""
|
|
409
667
|
context = default_context()
|
|
410
668
|
try:
|
|
411
|
-
log_api_event(
|
|
669
|
+
log_api_event("rename", dict(state_loader_mode=context.state_loader_mode()))
|
|
412
670
|
saveas_or_rename(input, rename=True)
|
|
413
671
|
except RecceException as e:
|
|
414
672
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -421,7 +679,7 @@ async def export_handler():
|
|
|
421
679
|
"""
|
|
422
680
|
context = default_context()
|
|
423
681
|
try:
|
|
424
|
-
log_api_event(
|
|
682
|
+
log_api_event("export", dict(state_loader_mode=context.state_loader_mode()))
|
|
425
683
|
return context.export_state().to_json()
|
|
426
684
|
except RecceException as e:
|
|
427
685
|
raise HTTPException(status_code=400, detail=e.message)
|
|
@@ -429,17 +687,16 @@ async def export_handler():
|
|
|
429
687
|
|
|
430
688
|
@app.post("/api/import", status_code=200)
|
|
431
689
|
async def import_handler(
|
|
432
|
-
file: Annotated[UploadFile, Form()],
|
|
433
|
-
checks_only: Annotated[bool, Form()],
|
|
434
|
-
background_tasks: BackgroundTasks
|
|
690
|
+
file: Annotated[UploadFile, Form()], checks_only: Annotated[bool, Form()], background_tasks: BackgroundTasks
|
|
435
691
|
):
|
|
436
692
|
"""
|
|
437
693
|
Import the recce state from the client.
|
|
438
694
|
"""
|
|
439
695
|
from recce.state import RecceState
|
|
696
|
+
|
|
440
697
|
context = default_context()
|
|
441
698
|
try:
|
|
442
|
-
log_api_event(
|
|
699
|
+
log_api_event("import", dict(state_loader_mode=context.state_loader_mode()))
|
|
443
700
|
content = await file.read()
|
|
444
701
|
state = RecceState.from_json(content)
|
|
445
702
|
|
|
@@ -473,16 +730,19 @@ async def sync_handler(input: SyncStateInput, response: Response, background_tas
|
|
|
473
730
|
context = default_context()
|
|
474
731
|
state_loader = context.state_loader
|
|
475
732
|
method = input.method
|
|
476
|
-
log_api_event(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
733
|
+
log_api_event(
|
|
734
|
+
"sync",
|
|
735
|
+
dict(
|
|
736
|
+
state_loader_mode=context.state_loader_mode(),
|
|
737
|
+
method=method,
|
|
738
|
+
),
|
|
739
|
+
)
|
|
480
740
|
|
|
481
741
|
if not method:
|
|
482
742
|
is_conflict = state_loader.check_conflict()
|
|
483
743
|
if is_conflict:
|
|
484
|
-
raise HTTPException(status_code=409, detail=
|
|
485
|
-
method =
|
|
744
|
+
raise HTTPException(status_code=409, detail="Conflict detected")
|
|
745
|
+
method = "overwrite"
|
|
486
746
|
|
|
487
747
|
is_syncing = state_loader.state_lock.locked()
|
|
488
748
|
if is_syncing:
|
|
@@ -532,7 +792,7 @@ async def share_state():
|
|
|
532
792
|
context = default_context()
|
|
533
793
|
state_loader = context.state_loader
|
|
534
794
|
|
|
535
|
-
file_name =
|
|
795
|
+
file_name = "recce_state.json"
|
|
536
796
|
if state_loader.state_file:
|
|
537
797
|
file_name = os.path.basename(state_loader.state_file)
|
|
538
798
|
|
|
@@ -568,8 +828,8 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
|
568
828
|
try:
|
|
569
829
|
while True:
|
|
570
830
|
data = await websocket.receive_text()
|
|
571
|
-
if data ==
|
|
572
|
-
await websocket.send_text(
|
|
831
|
+
if data == "ping":
|
|
832
|
+
await websocket.send_text("pong")
|
|
573
833
|
except WebSocketDisconnect:
|
|
574
834
|
clients.remove(websocket)
|
|
575
835
|
|
|
@@ -579,9 +839,39 @@ async def broadcast(data: str):
|
|
|
579
839
|
await client.send_text(data)
|
|
580
840
|
|
|
581
841
|
|
|
582
|
-
|
|
842
|
+
@app.post("/api/connect")
|
|
843
|
+
async def generate_connect_to_cloud_url(background_tasks: BackgroundTasks):
|
|
844
|
+
if is_callback_server_running():
|
|
845
|
+
return {"connection_url": get_connection_url()}
|
|
846
|
+
|
|
847
|
+
private_key, public_key = generate_key_pair()
|
|
848
|
+
connection_url, callback_port = prepare_connection_url(public_key)
|
|
849
|
+
|
|
850
|
+
background_tasks.add_task(connect_to_cloud_background_task, private_key, callback_port, connection_url)
|
|
851
|
+
return {
|
|
852
|
+
"connection_url": connection_url,
|
|
853
|
+
}
|
|
854
|
+
|
|
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
|
+
|
|
870
|
+
api_prefix = "/api"
|
|
583
871
|
app.include_router(check_router, prefix=api_prefix)
|
|
872
|
+
app.include_router(check_events_router, prefix=api_prefix)
|
|
584
873
|
app.include_router(run_router, prefix=api_prefix)
|
|
585
874
|
|
|
586
|
-
static_folder_path = Path(__file__).parent /
|
|
875
|
+
static_folder_path = Path(__file__).parent / "data"
|
|
876
|
+
|
|
587
877
|
app.mount("/", StaticFiles(directory=static_folder_path, html=True), name="static")
|